| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- package repository
- import (
- "context"
- "database/sql"
- "errors"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
- "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
- "github.com/Wei-Shaw/sub2api/internal/service"
- )
- // getPerformanceStats 获取 RPM 和 TPM(近5分钟平均值,可选按用户过滤)
- func (r *usageLogRepository) getPerformanceStats(ctx context.Context, userID int64) (rpm, tpm int64, err error) {
- fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
- query := `
- SELECT
- COUNT(*) as request_count,
- COALESCE(SUM(input_tokens + output_tokens), 0) as token_count
- FROM usage_logs
- WHERE created_at >= $1`
- args := []any{fiveMinutesAgo}
- if userID > 0 {
- query += " AND user_id = $2"
- args = append(args, userID)
- }
- var requestCount int64
- var tokenCount int64
- if err := scanSingleRow(ctx, r.sql, query, args, &requestCount, &tokenCount); err != nil {
- return 0, 0, err
- }
- return requestCount / 5, tokenCount / 5, nil
- }
- // UserStats 用户使用统计
- type UserStats struct {
- TotalRequests int64 `json:"total_requests"`
- TotalTokens int64 `json:"total_tokens"`
- TotalCost float64 `json:"total_cost"`
- InputTokens int64 `json:"input_tokens"`
- OutputTokens int64 `json:"output_tokens"`
- CacheReadTokens int64 `json:"cache_read_tokens"`
- }
- func (r *usageLogRepository) GetUserStats(ctx context.Context, userID int64, startTime, endTime time.Time) (*UserStats, error) {
- query := `
- SELECT
- COUNT(*) as total_requests,
- COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
- COALESCE(SUM(actual_cost), 0) as total_cost,
- COALESCE(SUM(input_tokens), 0) as input_tokens,
- COALESCE(SUM(output_tokens), 0) as output_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens
- FROM usage_logs
- WHERE user_id = $1 AND created_at >= $2 AND created_at < $3
- `
- stats := &UserStats{}
- if err := scanSingleRow(
- ctx,
- r.sql,
- query,
- []any{userID, startTime, endTime},
- &stats.TotalRequests,
- &stats.TotalTokens,
- &stats.TotalCost,
- &stats.InputTokens,
- &stats.OutputTokens,
- &stats.CacheReadTokens,
- ); err != nil {
- return nil, err
- }
- return stats, nil
- }
- // DashboardStats 仪表盘统计
- type DashboardStats = usagestats.DashboardStats
- func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardStats, error) {
- stats := &DashboardStats{}
- now := timezone.Now()
- todayStart := timezone.Today()
- if err := r.fillDashboardEntityStats(ctx, stats, todayStart, now); err != nil {
- return nil, err
- }
- if err := r.fillDashboardUsageStatsAggregated(ctx, stats, todayStart, now); err != nil {
- return nil, err
- }
- rpm, tpm, err := r.getPerformanceStats(ctx, 0)
- if err != nil {
- return nil, err
- }
- stats.Rpm = rpm
- stats.Tpm = tpm
- return stats, nil
- }
- func (r *usageLogRepository) GetDashboardStatsWithRange(ctx context.Context, start, end time.Time) (*DashboardStats, error) {
- startUTC := start.UTC()
- endUTC := end.UTC()
- if !endUTC.After(startUTC) {
- return nil, errors.New("统计时间范围无效")
- }
- stats := &DashboardStats{}
- now := timezone.Now()
- todayStart := timezone.Today()
- if err := r.fillDashboardEntityStats(ctx, stats, todayStart, now); err != nil {
- return nil, err
- }
- if err := r.fillDashboardUsageStatsFromUsageLogs(ctx, stats, startUTC, endUTC, todayStart, now); err != nil {
- return nil, err
- }
- rpm, tpm, err := r.getPerformanceStats(ctx, 0)
- if err != nil {
- return nil, err
- }
- stats.Rpm = rpm
- stats.Tpm = tpm
- return stats, nil
- }
- func (r *usageLogRepository) fillDashboardEntityStats(ctx context.Context, stats *DashboardStats, todayUTC, now time.Time) error {
- userStatsQuery := `
- SELECT
- COUNT(*) as total_users,
- COUNT(CASE WHEN created_at >= $1 THEN 1 END) as today_new_users
- FROM users
- WHERE deleted_at IS NULL
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- userStatsQuery,
- []any{todayUTC},
- &stats.TotalUsers,
- &stats.TodayNewUsers,
- ); err != nil {
- return err
- }
- apiKeyStatsQuery := `
- SELECT
- COUNT(*) as total_api_keys,
- COUNT(CASE WHEN status = $1 THEN 1 END) as active_api_keys
- FROM api_keys
- WHERE deleted_at IS NULL
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- apiKeyStatsQuery,
- []any{service.StatusActive},
- &stats.TotalAPIKeys,
- &stats.ActiveAPIKeys,
- ); err != nil {
- return err
- }
- accountStatsQuery := `
- SELECT
- COUNT(*) as total_accounts,
- COUNT(CASE WHEN status = $1 AND schedulable = true THEN 1 END) as normal_accounts,
- COUNT(CASE WHEN status = $2 THEN 1 END) as error_accounts,
- COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > $3 THEN 1 END) as ratelimit_accounts,
- COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > $4 THEN 1 END) as overload_accounts
- FROM accounts
- WHERE deleted_at IS NULL
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- accountStatsQuery,
- []any{service.StatusActive, service.StatusError, now, now},
- &stats.TotalAccounts,
- &stats.NormalAccounts,
- &stats.ErrorAccounts,
- &stats.RateLimitAccounts,
- &stats.OverloadAccounts,
- ); err != nil {
- return err
- }
- return nil
- }
- func (r *usageLogRepository) fillDashboardUsageStatsAggregated(ctx context.Context, stats *DashboardStats, todayUTC, now time.Time) error {
- totalStatsQuery := `
- SELECT
- COALESCE(SUM(total_requests), 0) as total_requests,
- COALESCE(SUM(input_tokens), 0) as total_input_tokens,
- COALESCE(SUM(output_tokens), 0) as total_output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
- COALESCE(SUM(total_cost), 0) as total_cost,
- COALESCE(SUM(actual_cost), 0) as total_actual_cost,
- COALESCE(SUM(account_cost), 0) as total_account_cost,
- COALESCE(SUM(total_duration_ms), 0) as total_duration_ms
- FROM usage_dashboard_daily
- `
- var totalDurationMs int64
- if err := scanSingleRow(
- ctx,
- r.sql,
- totalStatsQuery,
- nil,
- &stats.TotalRequests,
- &stats.TotalInputTokens,
- &stats.TotalOutputTokens,
- &stats.TotalCacheCreationTokens,
- &stats.TotalCacheReadTokens,
- &stats.TotalCost,
- &stats.TotalActualCost,
- &stats.TotalAccountCost,
- &totalDurationMs,
- ); err != nil {
- return err
- }
- stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
- if stats.TotalRequests > 0 {
- stats.AverageDurationMs = float64(totalDurationMs) / float64(stats.TotalRequests)
- }
- todayStatsQuery := `
- SELECT
- total_requests as today_requests,
- input_tokens as today_input_tokens,
- output_tokens as today_output_tokens,
- cache_creation_tokens as today_cache_creation_tokens,
- cache_read_tokens as today_cache_read_tokens,
- total_cost as today_cost,
- actual_cost as today_actual_cost,
- account_cost as today_account_cost,
- active_users as active_users
- FROM usage_dashboard_daily
- WHERE bucket_date = $1::date
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- todayStatsQuery,
- []any{todayUTC},
- &stats.TodayRequests,
- &stats.TodayInputTokens,
- &stats.TodayOutputTokens,
- &stats.TodayCacheCreationTokens,
- &stats.TodayCacheReadTokens,
- &stats.TodayCost,
- &stats.TodayActualCost,
- &stats.TodayAccountCost,
- &stats.ActiveUsers,
- ); err != nil {
- if err != sql.ErrNoRows {
- return err
- }
- }
- stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
- hourlyActiveQuery := `
- SELECT active_users
- FROM usage_dashboard_hourly
- WHERE bucket_start = $1
- `
- hourStart := now.In(timezone.Location()).Truncate(time.Hour)
- if err := scanSingleRow(ctx, r.sql, hourlyActiveQuery, []any{hourStart}, &stats.HourlyActiveUsers); err != nil {
- if err != sql.ErrNoRows {
- return err
- }
- }
- return nil
- }
- func (r *usageLogRepository) fillDashboardUsageStatsFromUsageLogs(ctx context.Context, stats *DashboardStats, startUTC, endUTC, todayUTC, now time.Time) error {
- todayEnd := todayUTC.Add(24 * time.Hour)
- combinedStatsQuery := `
- WITH scoped AS (
- SELECT
- created_at,
- input_tokens,
- output_tokens,
- cache_creation_tokens,
- cache_read_tokens,
- total_cost,
- actual_cost,
- COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1) AS account_cost,
- COALESCE(duration_ms, 0) AS duration_ms
- FROM usage_logs
- WHERE created_at >= LEAST($1::timestamptz, $3::timestamptz)
- AND created_at < GREATEST($2::timestamptz, $4::timestamptz)
- )
- SELECT
- COUNT(*) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz) AS total_requests,
- COALESCE(SUM(input_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_input_tokens,
- COALESCE(SUM(output_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_output_tokens,
- COALESCE(SUM(cache_creation_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cache_read_tokens,
- COALESCE(SUM(total_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_cost,
- COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_actual_cost,
- COALESCE(SUM(account_cost) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_account_cost,
- COALESCE(SUM(duration_ms) FILTER (WHERE created_at >= $1::timestamptz AND created_at < $2::timestamptz), 0) AS total_duration_ms,
- COUNT(*) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz) AS today_requests,
- COALESCE(SUM(input_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_input_tokens,
- COALESCE(SUM(output_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_output_tokens,
- COALESCE(SUM(cache_creation_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cache_read_tokens,
- COALESCE(SUM(total_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_cost,
- COALESCE(SUM(actual_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_actual_cost,
- COALESCE(SUM(account_cost) FILTER (WHERE created_at >= $3::timestamptz AND created_at < $4::timestamptz), 0) AS today_account_cost
- FROM scoped
- `
- var totalDurationMs int64
- if err := scanSingleRow(
- ctx,
- r.sql,
- combinedStatsQuery,
- []any{startUTC, endUTC, todayUTC, todayEnd},
- &stats.TotalRequests,
- &stats.TotalInputTokens,
- &stats.TotalOutputTokens,
- &stats.TotalCacheCreationTokens,
- &stats.TotalCacheReadTokens,
- &stats.TotalCost,
- &stats.TotalActualCost,
- &stats.TotalAccountCost,
- &totalDurationMs,
- &stats.TodayRequests,
- &stats.TodayInputTokens,
- &stats.TodayOutputTokens,
- &stats.TodayCacheCreationTokens,
- &stats.TodayCacheReadTokens,
- &stats.TodayCost,
- &stats.TodayActualCost,
- &stats.TodayAccountCost,
- ); err != nil {
- return err
- }
- stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
- if stats.TotalRequests > 0 {
- stats.AverageDurationMs = float64(totalDurationMs) / float64(stats.TotalRequests)
- }
- stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
- hourStart := now.UTC().Truncate(time.Hour)
- hourEnd := hourStart.Add(time.Hour)
- activeUsersQuery := `
- WITH scoped AS (
- SELECT user_id, created_at
- FROM usage_logs
- WHERE created_at >= LEAST($1::timestamptz, $3::timestamptz)
- AND created_at < GREATEST($2::timestamptz, $4::timestamptz)
- )
- SELECT
- COUNT(DISTINCT CASE WHEN created_at >= $1::timestamptz AND created_at < $2::timestamptz THEN user_id END) AS active_users,
- COUNT(DISTINCT CASE WHEN created_at >= $3::timestamptz AND created_at < $4::timestamptz THEN user_id END) AS hourly_active_users
- FROM scoped
- `
- if err := scanSingleRow(ctx, r.sql, activeUsersQuery, []any{todayUTC, todayEnd, hourStart, hourEnd}, &stats.ActiveUsers, &stats.HourlyActiveUsers); err != nil {
- return err
- }
- return nil
- }
- // UserDashboardStats 用户仪表盘统计
- type UserDashboardStats = usagestats.UserDashboardStats
- // PlatformDashboardStats 单平台用量明细
- type PlatformDashboardStats = usagestats.PlatformDashboardStats
- // GetUserDashboardStats 获取用户专属的仪表盘统计
- func (r *usageLogRepository) GetUserDashboardStats(ctx context.Context, userID int64) (*UserDashboardStats, error) {
- stats := &UserDashboardStats{}
- today := timezone.Today()
- // API Key 统计
- if err := scanSingleRow(
- ctx,
- r.sql,
- "SELECT COUNT(*) FROM api_keys WHERE user_id = $1 AND deleted_at IS NULL",
- []any{userID},
- &stats.TotalAPIKeys,
- ); err != nil {
- return nil, err
- }
- if err := scanSingleRow(
- ctx,
- r.sql,
- "SELECT COUNT(*) FROM api_keys WHERE user_id = $1 AND status = $2 AND deleted_at IS NULL",
- []any{userID, service.StatusActive},
- &stats.ActiveAPIKeys,
- ); err != nil {
- return nil, err
- }
- // 累计 Token 统计
- totalStatsQuery := `
- SELECT
- COUNT(*) as total_requests,
- COALESCE(SUM(input_tokens), 0) as total_input_tokens,
- COALESCE(SUM(output_tokens), 0) as total_output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
- COALESCE(SUM(total_cost), 0) as total_cost,
- COALESCE(SUM(actual_cost), 0) as total_actual_cost,
- COALESCE(AVG(duration_ms), 0) as avg_duration_ms
- FROM usage_logs
- WHERE user_id = $1
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- totalStatsQuery,
- []any{userID},
- &stats.TotalRequests,
- &stats.TotalInputTokens,
- &stats.TotalOutputTokens,
- &stats.TotalCacheCreationTokens,
- &stats.TotalCacheReadTokens,
- &stats.TotalCost,
- &stats.TotalActualCost,
- &stats.AverageDurationMs,
- ); err != nil {
- return nil, err
- }
- stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
- // 今日 Token 统计
- todayStatsQuery := `
- SELECT
- COUNT(*) as today_requests,
- COALESCE(SUM(input_tokens), 0) as today_input_tokens,
- COALESCE(SUM(output_tokens), 0) as today_output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens,
- COALESCE(SUM(total_cost), 0) as today_cost,
- COALESCE(SUM(actual_cost), 0) as today_actual_cost
- FROM usage_logs
- WHERE user_id = $1 AND created_at >= $2
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- todayStatsQuery,
- []any{userID, today},
- &stats.TodayRequests,
- &stats.TodayInputTokens,
- &stats.TodayOutputTokens,
- &stats.TodayCacheCreationTokens,
- &stats.TodayCacheReadTokens,
- &stats.TodayCost,
- &stats.TodayActualCost,
- ); err != nil {
- return nil, err
- }
- stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
- // 性能指标:RPM 和 TPM(最近1分钟,仅统计该用户的请求)
- rpm, tpm, err := r.getPerformanceStats(ctx, userID)
- if err != nil {
- return nil, err
- }
- stats.Rpm = rpm
- stats.Tpm = tpm
- // 按"有效平台"维度拆分(group.platform 优先,否则 account.platform)。
- // 与 ops 路径口径一致;HAVING 过滤掉无法确定平台的行(避免出现空字符串平台)。
- // 与上面 totalStatsQuery/todayStatsQuery 的总值可能略微差异,原因有二:
- // 1) 无平台归属的极少数行(group/account 都没 platform)会被 HAVING 排除;
- // 2) usageLogSuccessFilterUL 会把 actual_cost = 0 的失败 placeholder 行排除,
- // 而 totalStatsQuery/todayStatsQuery 没有这层过滤、会把这些行的 request 计数算进去。
- platformQuery := `
- SELECT
- ` + usageLogEffectivePlatformExpr + ` as platform,
- COUNT(*) as total_requests,
- COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens), 0) as total_tokens,
- COALESCE(SUM(ul.actual_cost), 0) as total_actual_cost,
- COUNT(*) FILTER (WHERE ul.created_at >= $2) as today_requests,
- COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens) FILTER (WHERE ul.created_at >= $2), 0) as today_tokens,
- COALESCE(SUM(ul.actual_cost) FILTER (WHERE ul.created_at >= $2), 0) as today_actual_cost
- FROM usage_logs ul
- LEFT JOIN groups g ON g.id = ul.group_id
- LEFT JOIN accounts a ON a.id = ul.account_id
- WHERE ul.user_id = $1
- AND ` + usageLogSuccessFilterUL + `
- GROUP BY ` + usageLogEffectivePlatformExpr + `
- HAVING ` + usageLogEffectivePlatformExpr + ` IS NOT NULL AND ` + usageLogEffectivePlatformExpr + ` <> ''
- ORDER BY total_actual_cost DESC
- `
- rows, err := r.sql.QueryContext(ctx, platformQuery, userID, today)
- if err != nil {
- return nil, err
- }
- for rows.Next() {
- var p PlatformDashboardStats
- if err := rows.Scan(
- &p.Platform,
- &p.TotalRequests,
- &p.TotalTokens,
- &p.TotalActualCost,
- &p.TodayRequests,
- &p.TodayTokens,
- &p.TodayActualCost,
- ); err != nil {
- _ = rows.Close()
- return nil, err
- }
- stats.ByPlatform = append(stats.ByPlatform, p)
- }
- if err := rows.Close(); err != nil {
- return nil, err
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return stats, nil
- }
- // getPerformanceStatsByAPIKey 获取指定 API Key 的 RPM 和 TPM(近5分钟平均值)
- func (r *usageLogRepository) getPerformanceStatsByAPIKey(ctx context.Context, apiKeyID int64) (rpm, tpm int64, err error) {
- fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
- query := `
- SELECT
- COUNT(*) as request_count,
- COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as token_count
- FROM usage_logs
- WHERE created_at >= $1 AND api_key_id = $2`
- args := []any{fiveMinutesAgo, apiKeyID}
- var requestCount int64
- var tokenCount int64
- if err := scanSingleRow(ctx, r.sql, query, args, &requestCount, &tokenCount); err != nil {
- return 0, 0, err
- }
- return requestCount / 5, tokenCount / 5, nil
- }
- // GetAPIKeyDashboardStats 获取指定 API Key 的仪表盘统计(按 api_key_id 过滤)
- func (r *usageLogRepository) GetAPIKeyDashboardStats(ctx context.Context, apiKeyID int64) (*UserDashboardStats, error) {
- stats := &UserDashboardStats{}
- today := timezone.Today()
- // API Key 维度不需要统计 key 数量,设为 1
- stats.TotalAPIKeys = 1
- stats.ActiveAPIKeys = 1
- // 累计 Token 统计
- totalStatsQuery := `
- SELECT
- COUNT(*) as total_requests,
- COALESCE(SUM(input_tokens), 0) as total_input_tokens,
- COALESCE(SUM(output_tokens), 0) as total_output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as total_cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as total_cache_read_tokens,
- COALESCE(SUM(total_cost), 0) as total_cost,
- COALESCE(SUM(actual_cost), 0) as total_actual_cost,
- COALESCE(AVG(duration_ms), 0) as avg_duration_ms
- FROM usage_logs
- WHERE api_key_id = $1
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- totalStatsQuery,
- []any{apiKeyID},
- &stats.TotalRequests,
- &stats.TotalInputTokens,
- &stats.TotalOutputTokens,
- &stats.TotalCacheCreationTokens,
- &stats.TotalCacheReadTokens,
- &stats.TotalCost,
- &stats.TotalActualCost,
- &stats.AverageDurationMs,
- ); err != nil {
- return nil, err
- }
- stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheCreationTokens + stats.TotalCacheReadTokens
- // 今日 Token 统计
- todayStatsQuery := `
- SELECT
- COUNT(*) as today_requests,
- COALESCE(SUM(input_tokens), 0) as today_input_tokens,
- COALESCE(SUM(output_tokens), 0) as today_output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as today_cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as today_cache_read_tokens,
- COALESCE(SUM(total_cost), 0) as today_cost,
- COALESCE(SUM(actual_cost), 0) as today_actual_cost
- FROM usage_logs
- WHERE api_key_id = $1 AND created_at >= $2
- `
- if err := scanSingleRow(
- ctx,
- r.sql,
- todayStatsQuery,
- []any{apiKeyID, today},
- &stats.TodayRequests,
- &stats.TodayInputTokens,
- &stats.TodayOutputTokens,
- &stats.TodayCacheCreationTokens,
- &stats.TodayCacheReadTokens,
- &stats.TodayCost,
- &stats.TodayActualCost,
- ); err != nil {
- return nil, err
- }
- stats.TodayTokens = stats.TodayInputTokens + stats.TodayOutputTokens + stats.TodayCacheCreationTokens + stats.TodayCacheReadTokens
- // 性能指标:RPM 和 TPM(最近5分钟,按 API Key 过滤)
- rpm, tpm, err := r.getPerformanceStatsByAPIKey(ctx, apiKeyID)
- if err != nil {
- return nil, err
- }
- stats.Rpm = rpm
- stats.Tpm = tpm
- return stats, nil
- }
|