Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
629 lines
21 KiB
Go
629 lines
21 KiB
Go
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
|
|
}
|