| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801 |
- package repository
- import (
- "context"
- "database/sql"
- "fmt"
- "strings"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
- )
- // TrendDataPoint represents a single point in trend data
- type TrendDataPoint = usagestats.TrendDataPoint
- // ModelStat represents usage statistics for a single model
- type ModelStat = usagestats.ModelStat
- // UserUsageTrendPoint represents user usage trend data point
- type UserUsageTrendPoint = usagestats.UserUsageTrendPoint
- // UserSpendingRankingItem represents a user spending ranking row.
- type UserSpendingRankingItem = usagestats.UserSpendingRankingItem
- type UserSpendingRankingResponse = usagestats.UserSpendingRankingResponse
- // APIKeyUsageTrendPoint represents API key usage trend data point
- type APIKeyUsageTrendPoint = usagestats.APIKeyUsageTrendPoint
- // GetAPIKeyUsageTrend returns usage trend data grouped by API key and date
- func (r *usageLogRepository) GetAPIKeyUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) (results []APIKeyUsageTrendPoint, err error) {
- dateFormat := safeDateFormat(granularity)
- query := fmt.Sprintf(`
- WITH top_keys AS (
- SELECT api_key_id
- FROM usage_logs
- WHERE created_at >= $1 AND created_at < $2
- GROUP BY api_key_id
- ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC
- LIMIT $3
- )
- SELECT
- TO_CHAR(u.created_at, '%s') as date,
- u.api_key_id,
- COALESCE(k.name, '') as key_name,
- COUNT(*) as requests,
- COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens
- FROM usage_logs u
- LEFT JOIN api_keys k ON u.api_key_id = k.id
- WHERE u.api_key_id IN (SELECT api_key_id FROM top_keys)
- AND u.created_at >= $4 AND u.created_at < $5
- GROUP BY date, u.api_key_id, k.name
- ORDER BY date ASC, tokens DESC
- `, dateFormat)
- rows, err := r.sql.QueryContext(ctx, query, startTime, endTime, limit, startTime, endTime)
- if err != nil {
- return nil, err
- }
- defer func() {
- // 保持主错误优先;仅在无错误时回传 Close 失败。
- // 同时清空返回值,避免误用不完整结果。
- if closeErr := rows.Close(); closeErr != nil && err == nil {
- err = closeErr
- results = nil
- }
- }()
- results = make([]APIKeyUsageTrendPoint, 0)
- for rows.Next() {
- var row APIKeyUsageTrendPoint
- if err = rows.Scan(&row.Date, &row.APIKeyID, &row.KeyName, &row.Requests, &row.Tokens); err != nil {
- return nil, err
- }
- results = append(results, row)
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return results, nil
- }
- // GetUserUsageTrend returns usage trend data grouped by user and date
- func (r *usageLogRepository) GetUserUsageTrend(ctx context.Context, startTime, endTime time.Time, granularity string, limit int) (results []UserUsageTrendPoint, err error) {
- dateFormat := safeDateFormat(granularity)
- query := fmt.Sprintf(`
- WITH top_users AS (
- SELECT user_id
- FROM usage_logs
- WHERE created_at >= $1 AND created_at < $2
- GROUP BY user_id
- ORDER BY SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens) DESC
- LIMIT $3
- )
- SELECT
- TO_CHAR(u.created_at, '%s') as date,
- u.user_id,
- COALESCE(us.email, '') as email,
- COALESCE(us.username, '') as username,
- COUNT(*) as requests,
- COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens,
- COALESCE(SUM(u.total_cost), 0) as cost,
- COALESCE(SUM(u.actual_cost), 0) as actual_cost
- FROM usage_logs u
- LEFT JOIN users us ON u.user_id = us.id
- WHERE u.user_id IN (SELECT user_id FROM top_users)
- AND u.created_at >= $4 AND u.created_at < $5
- GROUP BY date, u.user_id, us.email, us.username
- ORDER BY date ASC, tokens DESC
- `, dateFormat)
- rows, err := r.sql.QueryContext(ctx, query, startTime, endTime, limit, startTime, endTime)
- if err != nil {
- return nil, err
- }
- defer func() {
- // 保持主错误优先;仅在无错误时回传 Close 失败。
- // 同时清空返回值,避免误用不完整结果。
- if closeErr := rows.Close(); closeErr != nil && err == nil {
- err = closeErr
- results = nil
- }
- }()
- results = make([]UserUsageTrendPoint, 0)
- for rows.Next() {
- var row UserUsageTrendPoint
- if err = rows.Scan(&row.Date, &row.UserID, &row.Email, &row.Username, &row.Requests, &row.Tokens, &row.Cost, &row.ActualCost); err != nil {
- return nil, err
- }
- results = append(results, row)
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return results, nil
- }
- // GetUserSpendingRanking returns user spending ranking aggregated within the time range.
- func (r *usageLogRepository) GetUserSpendingRanking(ctx context.Context, startTime, endTime time.Time, limit int) (result *UserSpendingRankingResponse, err error) {
- if limit <= 0 {
- limit = 12
- }
- query := `
- WITH user_spend AS (
- SELECT
- u.user_id,
- COALESCE(us.email, '') as email,
- COALESCE(us.username, '') as username,
- COALESCE(SUM(u.actual_cost), 0) as actual_cost,
- COUNT(*) as requests,
- COALESCE(SUM(u.input_tokens + u.output_tokens + u.cache_creation_tokens + u.cache_read_tokens), 0) as tokens
- FROM usage_logs u
- LEFT JOIN users us ON u.user_id = us.id
- WHERE u.created_at >= $1 AND u.created_at < $2
- GROUP BY u.user_id, us.email, us.username
- ),
- ranked AS (
- SELECT
- user_id,
- email,
- username,
- actual_cost,
- requests,
- tokens,
- COALESCE(SUM(actual_cost) OVER (), 0) as total_actual_cost,
- COALESCE(SUM(requests) OVER (), 0) as total_requests,
- COALESCE(SUM(tokens) OVER (), 0) as total_tokens
- FROM user_spend
- ORDER BY actual_cost DESC, tokens DESC, user_id ASC
- LIMIT $3
- )
- SELECT
- user_id,
- email,
- username,
- actual_cost,
- requests,
- tokens,
- total_actual_cost,
- total_requests,
- total_tokens
- FROM ranked
- ORDER BY actual_cost DESC, tokens DESC, user_id ASC
- `
- rows, err := r.sql.QueryContext(ctx, query, startTime, endTime, limit)
- if err != nil {
- return nil, err
- }
- defer func() {
- if closeErr := rows.Close(); closeErr != nil && err == nil {
- err = closeErr
- result = nil
- }
- }()
- ranking := make([]UserSpendingRankingItem, 0)
- totalActualCost := 0.0
- totalRequests := int64(0)
- totalTokens := int64(0)
- for rows.Next() {
- var row UserSpendingRankingItem
- if err = rows.Scan(&row.UserID, &row.Email, &row.Username, &row.ActualCost, &row.Requests, &row.Tokens, &totalActualCost, &totalRequests, &totalTokens); err != nil {
- return nil, err
- }
- ranking = append(ranking, row)
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return &UserSpendingRankingResponse{
- Ranking: ranking,
- TotalActualCost: totalActualCost,
- TotalRequests: totalRequests,
- TotalTokens: totalTokens,
- }, nil
- }
- // GetUserUsageTrendByUserID 获取指定用户的使用趋势
- func (r *usageLogRepository) GetUserUsageTrendByUserID(ctx context.Context, userID int64, startTime, endTime time.Time, granularity string) (results []TrendDataPoint, err error) {
- dateFormat := safeDateFormat(granularity)
- query := fmt.Sprintf(`
- SELECT
- TO_CHAR(created_at, '%s') as date,
- COUNT(*) as requests,
- COALESCE(SUM(input_tokens), 0) as input_tokens,
- COALESCE(SUM(output_tokens), 0) as output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
- COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
- COALESCE(SUM(total_cost), 0) as cost,
- COALESCE(SUM(actual_cost), 0) as actual_cost
- FROM usage_logs
- WHERE user_id = $1 AND created_at >= $2 AND created_at < $3
- GROUP BY date
- ORDER BY date ASC
- `, dateFormat)
- rows, err := r.sql.QueryContext(ctx, query, userID, startTime, endTime)
- if err != nil {
- return nil, err
- }
- defer func() {
- // 保持主错误优先;仅在无错误时回传 Close 失败。
- // 同时清空返回值,避免误用不完整结果。
- 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
- }
- // GetUserModelStats 获取指定用户的模型统计
- func (r *usageLogRepository) GetUserModelStats(ctx context.Context, userID int64, startTime, endTime time.Time) (results []ModelStat, err error) {
- return r.getModelStatsWithFiltersBySource(ctx, startTime, endTime, userID, 0, 0, 0, "", nil, nil, nil, usagestats.ModelSourceRequested, "", nil, nil)
- }
- // 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) {
- return r.getUsageTrendWithFilters(ctx, startTime, endTime, granularity, userID, apiKeyID, accountID, groupID, model, "", requestType, stream, billingType, "", nil, nil)
- }
- func (r *usageLogRepository) GetUsageTrendWithUsageFilters(ctx context.Context, startTime, endTime time.Time, granularity string, filters UsageLogFilters) (results []TrendDataPoint, err error) {
- return r.getUsageTrendWithFilters(ctx, startTime, endTime, granularity, filters.UserID, filters.APIKeyID, filters.AccountID, filters.GroupID, filters.Model, filters.ModelFilterSource, filters.RequestType, filters.Stream, filters.BillingType, filters.BillingMode, filters.UpstreamModelMismatch, filters.NativeCompactionV2)
- }
- func (r *usageLogRepository) getUsageTrendWithFilters(ctx context.Context, startTime, endTime time.Time, granularity string, userID, apiKeyID, accountID, groupID int64, model string, modelSource string, requestType *int16, stream *bool, billingType *int8, billingMode string, upstreamModelMismatch *bool, nativeCompactionV2 *bool) (results []TrendDataPoint, err error) {
- if shouldUsePreaggregatedTrend(granularity, userID, apiKeyID, accountID, groupID, model, requestType, stream, billingType, billingMode, upstreamModelMismatch, nativeCompactionV2) {
- aggregated, aggregatedErr := r.getUsageTrendFromAggregates(ctx, startTime, endTime, granularity)
- if aggregatedErr == nil && len(aggregated) > 0 {
- return aggregated, nil
- }
- }
- dateFormat := safeDateFormat(granularity)
- query := fmt.Sprintf(`
- SELECT
- TO_CHAR(created_at, '%s') as date,
- COUNT(*) as requests,
- COALESCE(SUM(input_tokens), 0) as input_tokens,
- COALESCE(SUM(output_tokens), 0) as output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
- COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
- COALESCE(SUM(total_cost), 0) as cost,
- COALESCE(SUM(actual_cost), 0) as actual_cost
- FROM usage_logs
- WHERE created_at >= $1 AND created_at < $2
- `, dateFormat)
- args := []any{startTime, endTime}
- if userID > 0 {
- query += fmt.Sprintf(" AND user_id = $%d", len(args)+1)
- args = append(args, userID)
- }
- if apiKeyID > 0 {
- query += fmt.Sprintf(" AND api_key_id = $%d", len(args)+1)
- args = append(args, apiKeyID)
- }
- if accountID > 0 {
- query += fmt.Sprintf(" AND account_id = $%d", len(args)+1)
- args = append(args, accountID)
- }
- if groupID > 0 {
- query += fmt.Sprintf(" AND group_id = $%d", len(args)+1)
- args = append(args, groupID)
- }
- query, args = appendUsageLogModelQueryFilter(query, args, model, modelSource)
- query, args = appendRequestTypeOrStreamQueryFilter(query, args, requestType, stream)
- query, args = appendNativeCompactionV2QueryFilter(query, args, nativeCompactionV2, "")
- if billingType != nil {
- query += fmt.Sprintf(" AND billing_type = $%d", len(args)+1)
- args = append(args, int16(*billingType))
- }
- query, args = appendUsageLogBillingModeQueryFilter(query, args, billingMode, "")
- if upstreamModelMismatch != nil {
- query += " AND " + upstreamModelMismatchCondition("upstream_model_mismatch", *upstreamModelMismatch)
- }
- query += " GROUP BY date ORDER BY date ASC"
- rows, err := r.sql.QueryContext(ctx, query, args...)
- if err != nil {
- return nil, err
- }
- defer func() {
- // 保持主错误优先;仅在无错误时回传 Close 失败。
- // 同时清空返回值,避免误用不完整结果。
- 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
- }
- func shouldUsePreaggregatedTrend(granularity string, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8, billingMode string, upstreamModelMismatch *bool, nativeCompactionV2 *bool) bool {
- if granularity != "day" && granularity != "hour" {
- return false
- }
- return userID == 0 &&
- apiKeyID == 0 &&
- accountID == 0 &&
- groupID == 0 &&
- model == "" &&
- requestType == nil &&
- stream == nil &&
- billingType == nil &&
- billingMode == "" &&
- upstreamModelMismatch == nil &&
- nativeCompactionV2 == 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,
- (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,
- (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) {
- return r.getModelStatsWithFiltersBySource(ctx, startTime, endTime, userID, apiKeyID, accountID, groupID, "", requestType, stream, billingType, usagestats.ModelSourceRequested, "", nil, nil)
- }
- // GetModelStatsWithFiltersBySource returns model statistics with optional filters and model source dimension.
- // source: requested | upstream | mapping.
- func (r *usageLogRepository) GetModelStatsWithFiltersBySource(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8, source string) (results []ModelStat, err error) {
- return r.getModelStatsWithFiltersBySource(ctx, startTime, endTime, userID, apiKeyID, accountID, groupID, "", requestType, stream, billingType, source, "", nil, nil)
- }
- func (r *usageLogRepository) GetModelStatsWithUsageFiltersBySource(ctx context.Context, startTime, endTime time.Time, filters UsageLogFilters, source string) (results []ModelStat, err error) {
- return r.getModelStatsWithFiltersBySource(ctx, startTime, endTime, filters.UserID, filters.APIKeyID, filters.AccountID, filters.GroupID, filters.Model, filters.RequestType, filters.Stream, filters.BillingType, source, filters.BillingMode, filters.UpstreamModelMismatch, filters.NativeCompactionV2)
- }
- func (r *usageLogRepository) getModelStatsWithFiltersBySource(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8, source string, billingMode string, upstreamModelMismatch *bool, nativeCompactionV2 *bool) (results []ModelStat, err error) {
- actualCostExpr := "COALESCE(SUM(actual_cost), 0) as actual_cost"
- // 当仅按 account_id 聚合时,实际费用使用账号倍率(total_cost * account_rate_multiplier)。
- if accountID > 0 && userID == 0 && apiKeyID == 0 {
- actualCostExpr = "COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as actual_cost"
- }
- accountCostExpr := "COALESCE(SUM(COALESCE(account_stats_cost, total_cost) * COALESCE(account_rate_multiplier, 1)), 0) as account_cost"
- modelExpr := resolveModelDimensionExpression(source)
- query := fmt.Sprintf(`
- SELECT
- %s as model,
- COUNT(*) as requests,
- COALESCE(SUM(input_tokens), 0) as input_tokens,
- COALESCE(SUM(output_tokens), 0) as output_tokens,
- COALESCE(SUM(cache_creation_tokens), 0) as cache_creation_tokens,
- COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
- COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as total_tokens,
- COALESCE(SUM(total_cost), 0) as cost,
- %s,
- %s
- FROM usage_logs
- WHERE created_at >= $1 AND created_at < $2
- `, modelExpr, actualCostExpr, accountCostExpr)
- args := []any{startTime, endTime}
- if userID > 0 {
- query += fmt.Sprintf(" AND user_id = $%d", len(args)+1)
- args = append(args, userID)
- }
- if apiKeyID > 0 {
- query += fmt.Sprintf(" AND api_key_id = $%d", len(args)+1)
- args = append(args, apiKeyID)
- }
- if accountID > 0 {
- query += fmt.Sprintf(" AND account_id = $%d", len(args)+1)
- args = append(args, accountID)
- }
- if groupID > 0 {
- query += fmt.Sprintf(" AND group_id = $%d", len(args)+1)
- args = append(args, groupID)
- }
- if strings.TrimSpace(model) != "" {
- query += fmt.Sprintf(" AND %s = $%d", modelExpr, len(args)+1)
- args = append(args, model)
- }
- query, args = appendRequestTypeOrStreamQueryFilter(query, args, requestType, stream)
- query, args = appendNativeCompactionV2QueryFilter(query, args, nativeCompactionV2, "")
- if billingType != nil {
- query += fmt.Sprintf(" AND billing_type = $%d", len(args)+1)
- args = append(args, int16(*billingType))
- }
- query, args = appendUsageLogBillingModeQueryFilter(query, args, billingMode, "")
- if upstreamModelMismatch != nil {
- query += " AND " + upstreamModelMismatchCondition("upstream_model_mismatch", *upstreamModelMismatch)
- }
- query += fmt.Sprintf(" GROUP BY %s ORDER BY total_tokens DESC", modelExpr)
- rows, err := r.sql.QueryContext(ctx, query, args...)
- if err != nil {
- return nil, err
- }
- defer func() {
- // 保持主错误优先;仅在无错误时回传 Close 失败。
- // 同时清空返回值,避免误用不完整结果。
- if closeErr := rows.Close(); closeErr != nil && err == nil {
- err = closeErr
- results = nil
- }
- }()
- results, err = scanModelStatsRows(rows)
- if err != nil {
- return nil, err
- }
- return results, nil
- }
- // GetGroupStatsWithFilters returns group usage statistics with optional filters
- func (r *usageLogRepository) GetGroupStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, requestType *int16, stream *bool, billingType *int8) (results []usagestats.GroupStat, err error) {
- return r.getGroupStatsWithFilters(ctx, startTime, endTime, userID, apiKeyID, accountID, groupID, "", requestType, stream, billingType, "", nil, nil)
- }
- func (r *usageLogRepository) GetGroupStatsWithUsageFilters(ctx context.Context, startTime, endTime time.Time, filters UsageLogFilters) (results []usagestats.GroupStat, err error) {
- return r.getGroupStatsWithFilters(ctx, startTime, endTime, filters.UserID, filters.APIKeyID, filters.AccountID, filters.GroupID, filters.Model, filters.RequestType, filters.Stream, filters.BillingType, filters.BillingMode, filters.UpstreamModelMismatch, filters.NativeCompactionV2)
- }
- func (r *usageLogRepository) getGroupStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID, groupID int64, model string, requestType *int16, stream *bool, billingType *int8, billingMode string, upstreamModelMismatch *bool, nativeCompactionV2 *bool) (results []usagestats.GroupStat, err error) {
- query := `
- SELECT
- COALESCE(ul.group_id, 0) as group_id,
- COALESCE(g.name, '') as group_name,
- COUNT(*) as requests,
- COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens), 0) as total_tokens,
- COALESCE(SUM(ul.total_cost), 0) as cost,
- COALESCE(SUM(ul.actual_cost), 0) as actual_cost,
- COALESCE(SUM(COALESCE(ul.account_stats_cost, ul.total_cost) * COALESCE(ul.account_rate_multiplier, 1)), 0) as account_cost
- FROM usage_logs ul
- LEFT JOIN groups g ON g.id = ul.group_id
- WHERE ul.created_at >= $1 AND ul.created_at < $2
- `
- args := []any{startTime, endTime}
- if userID > 0 {
- query += fmt.Sprintf(" AND ul.user_id = $%d", len(args)+1)
- args = append(args, userID)
- }
- if apiKeyID > 0 {
- query += fmt.Sprintf(" AND ul.api_key_id = $%d", len(args)+1)
- args = append(args, apiKeyID)
- }
- if accountID > 0 {
- query += fmt.Sprintf(" AND ul.account_id = $%d", len(args)+1)
- args = append(args, accountID)
- }
- if groupID > 0 {
- query += fmt.Sprintf(" AND ul.group_id = $%d", len(args)+1)
- args = append(args, groupID)
- }
- if strings.TrimSpace(model) != "" {
- modelExpr := resolveModelDimensionExpressionWithAlias(usagestats.ModelSourceRequested, "ul")
- query += fmt.Sprintf(" AND %s = $%d", modelExpr, len(args)+1)
- args = append(args, model)
- }
- query, args = appendRequestTypeOrStreamQueryFilter(query, args, requestType, stream)
- query, args = appendNativeCompactionV2QueryFilter(query, args, nativeCompactionV2, "ul")
- if billingType != nil {
- query += fmt.Sprintf(" AND ul.billing_type = $%d", len(args)+1)
- args = append(args, int16(*billingType))
- }
- query, args = appendUsageLogBillingModeQueryFilter(query, args, billingMode, "ul")
- if upstreamModelMismatch != nil {
- query += " AND " + upstreamModelMismatchCondition("ul.upstream_model_mismatch", *upstreamModelMismatch)
- }
- query += " GROUP BY ul.group_id, g.name ORDER BY total_tokens DESC"
- 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 = make([]usagestats.GroupStat, 0)
- for rows.Next() {
- var row usagestats.GroupStat
- if err := rows.Scan(
- &row.GroupID,
- &row.GroupName,
- &row.Requests,
- &row.TotalTokens,
- &row.Cost,
- &row.ActualCost,
- &row.AccountCost,
- ); err != nil {
- return nil, err
- }
- results = append(results, row)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return results, nil
- }
- // GetUserBreakdownStats returns per-user usage breakdown within a specific dimension.
- func (r *usageLogRepository) GetUserBreakdownStats(ctx context.Context, startTime, endTime time.Time, dim usagestats.UserBreakdownDimension, limit int) (results []usagestats.UserBreakdownItem, err error) {
- query := `
- SELECT
- COALESCE(ul.user_id, 0) as user_id,
- COALESCE(u.email, '') as email,
- COUNT(*) as requests,
- COALESCE(SUM(ul.input_tokens), 0) as input_tokens,
- COALESCE(SUM(ul.output_tokens), 0) as output_tokens,
- COALESCE(SUM(ul.cache_creation_tokens + ul.cache_read_tokens), 0) as cache_tokens,
- COALESCE(SUM(ul.input_tokens + ul.output_tokens + ul.cache_creation_tokens + ul.cache_read_tokens), 0) as total_tokens,
- COALESCE(SUM(ul.total_cost), 0) as cost,
- COALESCE(SUM(ul.actual_cost), 0) as actual_cost,
- COALESCE(SUM(COALESCE(ul.account_stats_cost, ul.total_cost) * COALESCE(ul.account_rate_multiplier, 1)), 0) as account_cost
- FROM usage_logs ul
- LEFT JOIN users u ON u.id = ul.user_id
- WHERE ul.created_at >= $1 AND ul.created_at < $2
- `
- args := []any{startTime, endTime}
- if dim.GroupID > 0 {
- query += fmt.Sprintf(" AND ul.group_id = $%d", len(args)+1)
- args = append(args, dim.GroupID)
- }
- if dim.Model != "" {
- query += fmt.Sprintf(" AND %s = $%d", resolveModelDimensionExpression(dim.ModelType), len(args)+1)
- args = append(args, dim.Model)
- }
- if dim.Endpoint != "" {
- col := resolveEndpointColumn(dim.EndpointType)
- query += fmt.Sprintf(" AND %s = $%d", col, len(args)+1)
- args = append(args, dim.Endpoint)
- }
- if dim.UserID > 0 {
- query += fmt.Sprintf(" AND ul.user_id = $%d", len(args)+1)
- args = append(args, dim.UserID)
- }
- if dim.APIKeyID > 0 {
- query += fmt.Sprintf(" AND ul.api_key_id = $%d", len(args)+1)
- args = append(args, dim.APIKeyID)
- }
- if dim.AccountID > 0 {
- query += fmt.Sprintf(" AND ul.account_id = $%d", len(args)+1)
- args = append(args, dim.AccountID)
- }
- if dim.RequestType != nil {
- condition, conditionArgs := buildRequestTypeFilterConditionWithAlias(len(args)+1, *dim.RequestType, "ul")
- query += " AND " + condition
- args = append(args, conditionArgs...)
- }
- if dim.Stream != nil {
- query += fmt.Sprintf(" AND ul.stream = $%d", len(args)+1)
- args = append(args, *dim.Stream)
- }
- query, args = appendNativeCompactionV2QueryFilter(query, args, dim.NativeCompactionV2, "ul")
- if dim.BillingType != nil {
- query += fmt.Sprintf(" AND ul.billing_type = $%d", len(args)+1)
- args = append(args, *dim.BillingType)
- }
- // ORDER BY 列来自固定 allowlist(非用户原样字符串),避免 SQL 注入。
- orderBy := "actual_cost"
- switch dim.SortBy {
- case "total_tokens", "input_tokens", "output_tokens", "cache_tokens", "requests", "cost", "actual_cost":
- orderBy = dim.SortBy
- }
- query += " GROUP BY ul.user_id, u.email ORDER BY " + orderBy + " DESC"
- if limit > 0 {
- query += fmt.Sprintf(" LIMIT %d", limit)
- }
- 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 = make([]usagestats.UserBreakdownItem, 0)
- for rows.Next() {
- var row usagestats.UserBreakdownItem
- if err := rows.Scan(
- &row.UserID,
- &row.Email,
- &row.Requests,
- &row.InputTokens,
- &row.OutputTokens,
- &row.CacheTokens,
- &row.TotalTokens,
- &row.Cost,
- &row.ActualCost,
- &row.AccountCost,
- ); err != nil {
- return nil, err
- }
- results = append(results, row)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return results, nil
- }
- // GetAllGroupUsageSummary 返回所有分组在服务端配置时区内的今日、昨日与当前保留记录累计金额。
- func (r *usageLogRepository) GetAllGroupUsageSummary(ctx context.Context, todayStart time.Time) ([]usagestats.GroupUsageSummary, error) {
- return r.getAllGroupUsageSummaryFromRollups(ctx, todayStart)
- }
- // resolveModelDimensionExpression maps model source type to a safe SQL expression.
- func resolveModelDimensionExpression(modelType string) string {
- return resolveModelDimensionExpressionWithAlias(modelType, "")
- }
- func resolveModelDimensionExpressionWithAlias(modelType, alias string) string {
- column := func(name string) string {
- if alias == "" {
- return name
- }
- return alias + "." + name
- }
- requestedExpr := fmt.Sprintf("COALESCE(NULLIF(TRIM(%s), ''), %s)", column("requested_model"), column("model"))
- upstreamExpr := fmt.Sprintf("COALESCE(NULLIF(TRIM(%s), ''), %s)", column("upstream_model"), column("model"))
- switch usagestats.NormalizeModelSource(modelType) {
- case usagestats.ModelSourceUpstream:
- return upstreamExpr
- case usagestats.ModelSourceMapping:
- return fmt.Sprintf("(%s || ' -> ' || %s)", requestedExpr, upstreamExpr)
- default:
- return requestedExpr
- }
- }
- func scanTrendRows(rows *sql.Rows) ([]TrendDataPoint, error) {
- results := make([]TrendDataPoint, 0)
- for rows.Next() {
- var row TrendDataPoint
- if err := rows.Scan(
- &row.Date,
- &row.Requests,
- &row.InputTokens,
- &row.OutputTokens,
- &row.CacheCreationTokens,
- &row.CacheReadTokens,
- &row.TotalTokens,
- &row.Cost,
- &row.ActualCost,
- ); err != nil {
- return nil, err
- }
- results = append(results, row)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return results, nil
- }
- func scanModelStatsRows(rows *sql.Rows) ([]ModelStat, error) {
- results := make([]ModelStat, 0)
- for rows.Next() {
- var row ModelStat
- if err := rows.Scan(
- &row.Model,
- &row.Requests,
- &row.InputTokens,
- &row.OutputTokens,
- &row.CacheCreationTokens,
- &row.CacheReadTokens,
- &row.TotalTokens,
- &row.Cost,
- &row.ActualCost,
- &row.AccountCost,
- ); err != nil {
- return nil, err
- }
- results = append(results, row)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return results, nil
- }
|