| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- package repository
- import (
- "context"
- "database/sql"
- "encoding/json"
- "fmt"
- "strings"
- "time"
- dbaccount "github.com/Wei-Shaw/sub2api/ent/account"
- dbapikey "github.com/Wei-Shaw/sub2api/ent/apikey"
- dbgroup "github.com/Wei-Shaw/sub2api/ent/group"
- "github.com/Wei-Shaw/sub2api/ent/schema/mixins"
- dbuser "github.com/Wei-Shaw/sub2api/ent/user"
- dbusersub "github.com/Wei-Shaw/sub2api/ent/usersubscription"
- "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
- "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
- "github.com/Wei-Shaw/sub2api/internal/service"
- )
- const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, requested_model, upstream_model, upstream_response_model, upstream_model_mismatch, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, image_output_tokens, image_output_cost, image_input_tokens, image_input_cost, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, request_type, stream, openai_ws_mode, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, image_input_size, image_output_size, image_size_source, image_size_breakdown, video_count, video_resolution, video_duration_seconds, service_tier, reasoning_effort, requested_reasoning_effort, inbound_endpoint, upstream_endpoint, cache_ttl_overridden, long_context_billing_applied, channel_id, model_mapping_chain, billing_tier, billing_mode, account_stats_cost, upstream_request_id, session_id, native_compaction_v2, created_at"
- func (r *usageLogRepository) GetByID(ctx context.Context, id int64) (log *service.UsageLog, err error) {
- query := "SELECT " + usageLogSelectColumns + " FROM usage_logs WHERE id = $1"
- rows, err := r.sql.QueryContext(ctx, query, id)
- if err != nil {
- return nil, err
- }
- defer func() {
- // 保持主错误优先;仅在无错误时回传 Close 失败。
- // 同时清空返回值,避免误用不完整结果。
- if closeErr := rows.Close(); closeErr != nil && err == nil {
- err = closeErr
- log = nil
- }
- }()
- if !rows.Next() {
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return nil, service.ErrUsageLogNotFound
- }
- log, err = scanUsageLog(rows)
- if err != nil {
- return nil, err
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return log, nil
- }
- func (r *usageLogRepository) ListByUser(ctx context.Context, userID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return r.listUsageLogsWithPagination(ctx, "WHERE user_id = $1", []any{userID}, params)
- }
- func (r *usageLogRepository) ListByAPIKey(ctx context.Context, apiKeyID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return r.listUsageLogsWithPagination(ctx, "WHERE api_key_id = $1", []any{apiKeyID}, params)
- }
- func (r *usageLogRepository) ListByAccount(ctx context.Context, accountID int64, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- return r.listUsageLogsWithPagination(ctx, "WHERE account_id = $1", []any{accountID}, params)
- }
- func (r *usageLogRepository) ListByUserAndTimeRange(ctx context.Context, userID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- query := "SELECT " + usageLogSelectColumns + " FROM usage_logs WHERE user_id = $1 AND created_at >= $2 AND created_at < $3 ORDER BY id DESC LIMIT 10000"
- logs, err := r.queryUsageLogs(ctx, query, userID, startTime, endTime)
- return logs, nil, err
- }
- func (r *usageLogRepository) ListByAPIKeyAndTimeRange(ctx context.Context, apiKeyID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- query := "SELECT " + usageLogSelectColumns + " FROM usage_logs WHERE api_key_id = $1 AND created_at >= $2 AND created_at < $3 ORDER BY id DESC LIMIT 10000"
- logs, err := r.queryUsageLogs(ctx, query, apiKeyID, startTime, endTime)
- return logs, nil, err
- }
- func (r *usageLogRepository) ListByAccountAndTimeRange(ctx context.Context, accountID int64, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- query := "SELECT " + usageLogSelectColumns + " FROM usage_logs WHERE account_id = $1 AND created_at >= $2 AND created_at < $3 ORDER BY id DESC LIMIT 10000"
- logs, err := r.queryUsageLogs(ctx, query, accountID, startTime, endTime)
- return logs, nil, err
- }
- func (r *usageLogRepository) ListByModelAndTimeRange(ctx context.Context, modelName string, startTime, endTime time.Time) ([]service.UsageLog, *pagination.PaginationResult, error) {
- query := fmt.Sprintf("SELECT %s FROM usage_logs WHERE %s = $1 AND created_at >= $2 AND created_at < $3 ORDER BY id DESC LIMIT 10000", usageLogSelectColumns, rawUsageLogModelColumn)
- logs, err := r.queryUsageLogs(ctx, query, modelName, startTime, endTime)
- return logs, nil, err
- }
- func (r *usageLogRepository) Delete(ctx context.Context, id int64) error {
- _, err := r.sql.ExecContext(ctx, "DELETE FROM usage_logs WHERE id = $1", id)
- return err
- }
- // UsageLogFilters represents filters for usage log queries
- type UsageLogFilters = usagestats.UsageLogFilters
- // ListWithFilters lists usage logs with optional filters (for admin)
- func (r *usageLogRepository) ListWithFilters(ctx context.Context, params pagination.PaginationParams, filters UsageLogFilters) ([]service.UsageLog, *pagination.PaginationResult, error) {
- conditions := make([]string, 0, 9)
- args := make([]any, 0, 9)
- if filters.UserID > 0 {
- conditions = append(conditions, fmt.Sprintf("user_id = $%d", len(args)+1))
- args = append(args, filters.UserID)
- }
- if filters.APIKeyID > 0 {
- conditions = append(conditions, fmt.Sprintf("api_key_id = $%d", len(args)+1))
- args = append(args, filters.APIKeyID)
- }
- if filters.AccountID > 0 {
- conditions = append(conditions, fmt.Sprintf("account_id = $%d", len(args)+1))
- args = append(args, filters.AccountID)
- }
- if filters.GroupID > 0 {
- conditions = append(conditions, fmt.Sprintf("group_id = $%d", len(args)+1))
- args = append(args, filters.GroupID)
- }
- if requestID := strings.TrimSpace(filters.RequestID); requestID != "" {
- conditions = append(conditions, fmt.Sprintf("request_id = $%d", len(args)+1))
- args = append(args, requestID)
- }
- conditions, args = appendUsageLogModelWhereCondition(conditions, args, filters.Model, filters.ModelFilterSource)
- conditions, args = appendRequestTypeOrStreamWhereCondition(conditions, args, filters.RequestType, filters.Stream)
- conditions, args = appendNativeCompactionV2WhereCondition(conditions, args, filters.NativeCompactionV2, "")
- if filters.BillingType != nil {
- conditions = append(conditions, fmt.Sprintf("billing_type = $%d", len(args)+1))
- args = append(args, int16(*filters.BillingType))
- }
- conditions, args = appendUsageLogBillingModeWhereCondition(conditions, args, filters.BillingMode)
- if filters.UpstreamModelMismatch != nil {
- conditions = append(conditions, upstreamModelMismatchCondition("upstream_model_mismatch", *filters.UpstreamModelMismatch))
- }
- if filters.StartTime != nil {
- conditions = append(conditions, fmt.Sprintf("created_at >= $%d", len(args)+1))
- args = append(args, *filters.StartTime)
- }
- if filters.EndTime != nil {
- conditions = append(conditions, fmt.Sprintf("created_at < $%d", len(args)+1))
- args = append(args, *filters.EndTime)
- }
- whereClause := buildWhere(conditions)
- var (
- logs []service.UsageLog
- page *pagination.PaginationResult
- err error
- )
- if shouldUseFastUsageLogTotal(filters) {
- logs, page, err = r.listUsageLogsWithFastPagination(ctx, whereClause, args, params)
- } else {
- logs, page, err = r.listUsageLogsWithPagination(ctx, whereClause, args, params)
- }
- if err != nil {
- return nil, nil, err
- }
- if err := r.hydrateUsageLogAssociations(ctx, logs); err != nil {
- return nil, nil, err
- }
- return logs, page, nil
- }
- func upstreamModelMismatchCondition(column string, mismatch bool) string {
- if mismatch {
- return column + " IS TRUE"
- }
- return column + " IS FALSE"
- }
- func shouldUseFastUsageLogTotal(filters UsageLogFilters) bool {
- if filters.ExactTotal {
- return false
- }
- // 强选择过滤下记录集通常较小,保留精确总数。
- return filters.UserID == 0 && filters.APIKeyID == 0 && filters.AccountID == 0
- }
- func (r *usageLogRepository) listUsageLogsWithPagination(ctx context.Context, whereClause string, args []any, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- countQuery := "SELECT COUNT(*) FROM usage_logs " + whereClause
- var total int64
- if err := scanSingleRow(ctx, r.sql, countQuery, args, &total); err != nil {
- return nil, nil, err
- }
- limitPos := len(args) + 1
- offsetPos := len(args) + 2
- listArgs := append(append([]any{}, args...), params.Limit(), params.Offset())
- query := fmt.Sprintf("SELECT %s FROM usage_logs %s ORDER BY %s LIMIT $%d OFFSET $%d", usageLogSelectColumns, whereClause, usageLogOrderBy(params), limitPos, offsetPos)
- logs, err := r.queryUsageLogs(ctx, query, listArgs...)
- if err != nil {
- return nil, nil, err
- }
- return logs, paginationResultFromTotal(total, params), nil
- }
- func (r *usageLogRepository) listUsageLogsWithFastPagination(ctx context.Context, whereClause string, args []any, params pagination.PaginationParams) ([]service.UsageLog, *pagination.PaginationResult, error) {
- limit := params.Limit()
- offset := params.Offset()
- limitPos := len(args) + 1
- offsetPos := len(args) + 2
- listArgs := append(append([]any{}, args...), limit+1, offset)
- query := fmt.Sprintf("SELECT %s FROM usage_logs %s ORDER BY %s LIMIT $%d OFFSET $%d", usageLogSelectColumns, whereClause, usageLogOrderBy(params), limitPos, offsetPos)
- logs, err := r.queryUsageLogs(ctx, query, listArgs...)
- if err != nil {
- return nil, nil, err
- }
- hasMore := false
- if len(logs) > limit {
- hasMore = true
- logs = logs[:limit]
- }
- total := int64(offset) + int64(len(logs))
- if hasMore {
- // 只保证“还有下一页”,避免对超大表做全量 COUNT(*)。
- total = int64(offset) + int64(limit) + 1
- }
- return logs, paginationResultFromTotal(total, params), nil
- }
- func usageLogOrderBy(params pagination.PaginationParams) string {
- sortBy := strings.ToLower(strings.TrimSpace(params.SortBy))
- sortOrder := strings.ToUpper(params.NormalizedSortOrder(pagination.SortOrderDesc))
- var column string
- switch sortBy {
- case "model":
- column = "COALESCE(NULLIF(TRIM(requested_model), ''), model)"
- case "created_at":
- column = "created_at"
- default:
- column = "id"
- }
- if column == "id" {
- return fmt.Sprintf("id %s", sortOrder)
- }
- return fmt.Sprintf("%s %s, id %s", column, sortOrder, sortOrder)
- }
- func (r *usageLogRepository) queryUsageLogs(ctx context.Context, query string, args ...any) (logs []service.UsageLog, err error) {
- 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
- logs = nil
- }
- }()
- logs = make([]service.UsageLog, 0)
- for rows.Next() {
- var log *service.UsageLog
- log, err = scanUsageLog(rows)
- if err != nil {
- return nil, err
- }
- logs = append(logs, *log)
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return logs, nil
- }
- func (r *usageLogRepository) hydrateUsageLogAssociations(ctx context.Context, logs []service.UsageLog) error {
- // 关联数据使用 Ent 批量加载,避免把复杂 SQL 继续膨胀。
- if len(logs) == 0 {
- return nil
- }
- ids := collectUsageLogIDs(logs)
- users, err := r.loadUsers(ctx, ids.userIDs)
- if err != nil {
- return err
- }
- apiKeys, err := r.loadAPIKeys(ctx, ids.apiKeyIDs)
- if err != nil {
- return err
- }
- accounts, err := r.loadAccounts(ctx, ids.accountIDs)
- if err != nil {
- return err
- }
- groups, err := r.loadGroups(ctx, ids.groupIDs)
- if err != nil {
- return err
- }
- subs, err := r.loadSubscriptions(ctx, ids.subscriptionIDs)
- if err != nil {
- return err
- }
- for i := range logs {
- if user, ok := users[logs[i].UserID]; ok {
- logs[i].User = user
- }
- if key, ok := apiKeys[logs[i].APIKeyID]; ok {
- logs[i].APIKey = key
- }
- if acc, ok := accounts[logs[i].AccountID]; ok {
- logs[i].Account = acc
- }
- if logs[i].GroupID != nil {
- if group, ok := groups[*logs[i].GroupID]; ok {
- logs[i].Group = group
- }
- }
- if logs[i].SubscriptionID != nil {
- if sub, ok := subs[*logs[i].SubscriptionID]; ok {
- logs[i].Subscription = sub
- }
- }
- }
- return nil
- }
- type usageLogIDs struct {
- userIDs []int64
- apiKeyIDs []int64
- accountIDs []int64
- groupIDs []int64
- subscriptionIDs []int64
- }
- func collectUsageLogIDs(logs []service.UsageLog) usageLogIDs {
- idSet := func() map[int64]struct{} { return make(map[int64]struct{}) }
- userIDs := idSet()
- apiKeyIDs := idSet()
- accountIDs := idSet()
- groupIDs := idSet()
- subscriptionIDs := idSet()
- for i := range logs {
- userIDs[logs[i].UserID] = struct{}{}
- apiKeyIDs[logs[i].APIKeyID] = struct{}{}
- accountIDs[logs[i].AccountID] = struct{}{}
- if logs[i].GroupID != nil {
- groupIDs[*logs[i].GroupID] = struct{}{}
- }
- if logs[i].SubscriptionID != nil {
- subscriptionIDs[*logs[i].SubscriptionID] = struct{}{}
- }
- }
- return usageLogIDs{
- userIDs: setToSlice(userIDs),
- apiKeyIDs: setToSlice(apiKeyIDs),
- accountIDs: setToSlice(accountIDs),
- groupIDs: setToSlice(groupIDs),
- subscriptionIDs: setToSlice(subscriptionIDs),
- }
- }
- func (r *usageLogRepository) loadUsers(ctx context.Context, ids []int64) (map[int64]*service.User, error) {
- out := make(map[int64]*service.User)
- if len(ids) == 0 {
- return out, nil
- }
- // 无条件穿透软删除:ids 来自调用方已按 user_id 筛选的日志行;普通用户路径强制 UserID=本人(本人必为活跃用户),不会借此解析他人已删身份;仅 admin 路径可借此显示已删用户。
- models, err := r.client.User.Query().Where(dbuser.IDIn(ids...)).All(mixins.SkipSoftDelete(ctx))
- if err != nil {
- return nil, err
- }
- for _, m := range models {
- out[m.ID] = userEntityToService(m)
- }
- return out, nil
- }
- func (r *usageLogRepository) loadAPIKeys(ctx context.Context, ids []int64) (map[int64]*service.APIKey, error) {
- out := make(map[int64]*service.APIKey)
- if len(ids) == 0 {
- return out, nil
- }
- models, err := r.client.APIKey.Query().Where(dbapikey.IDIn(ids...)).All(ctx)
- if err != nil {
- return nil, err
- }
- for _, m := range models {
- out[m.ID] = apiKeyEntityToService(m)
- }
- return out, nil
- }
- func (r *usageLogRepository) loadAccounts(ctx context.Context, ids []int64) (map[int64]*service.Account, error) {
- out := make(map[int64]*service.Account)
- if len(ids) == 0 {
- return out, nil
- }
- models, err := r.client.Account.Query().Where(dbaccount.IDIn(ids...)).All(ctx)
- if err != nil {
- return nil, err
- }
- for _, m := range models {
- out[m.ID] = accountEntityToService(m)
- }
- return out, nil
- }
- func (r *usageLogRepository) loadGroups(ctx context.Context, ids []int64) (map[int64]*service.Group, error) {
- out := make(map[int64]*service.Group)
- if len(ids) == 0 {
- return out, nil
- }
- models, err := r.client.Group.Query().Where(dbgroup.IDIn(ids...)).All(ctx)
- if err != nil {
- return nil, err
- }
- for _, m := range models {
- out[m.ID] = groupEntityToService(m)
- }
- return out, nil
- }
- func (r *usageLogRepository) loadSubscriptions(ctx context.Context, ids []int64) (map[int64]*service.UserSubscription, error) {
- out := make(map[int64]*service.UserSubscription)
- if len(ids) == 0 {
- return out, nil
- }
- models, err := r.client.UserSubscription.Query().Where(dbusersub.IDIn(ids...)).All(ctx)
- if err != nil {
- return nil, err
- }
- for _, m := range models {
- out[m.ID] = userSubscriptionEntityToService(m)
- }
- return out, nil
- }
- func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, error) {
- var (
- id int64
- userID int64
- apiKeyID int64
- accountID int64
- requestID sql.NullString
- model string
- requestedModel sql.NullString
- upstreamModel sql.NullString
- upstreamResponseModel sql.NullString
- upstreamModelMismatch sql.NullBool
- groupID sql.NullInt64
- subscriptionID sql.NullInt64
- inputTokens int
- outputTokens int
- cacheCreationTokens int
- cacheReadTokens int
- cacheCreation5m int
- cacheCreation1h int
- imageOutputTokens int
- imageOutputCost float64
- imageInputTokens int
- imageInputCost float64
- inputCost float64
- outputCost float64
- cacheCreationCost float64
- cacheReadCost float64
- totalCost float64
- actualCost float64
- rateMultiplier float64
- accountRateMultiplier sql.NullFloat64
- billingType int16
- requestTypeRaw int16
- stream bool
- openaiWSMode bool
- durationMs sql.NullInt64
- firstTokenMs sql.NullInt64
- userAgent sql.NullString
- ipAddress sql.NullString
- imageCount int
- imageSize sql.NullString
- imageInputSize sql.NullString
- imageOutputSize sql.NullString
- imageSizeSource sql.NullString
- imageSizeBreakdown sql.NullString
- videoCount int
- videoResolution sql.NullString
- videoDurationSeconds sql.NullInt64
- serviceTier sql.NullString
- reasoningEffort sql.NullString
- requestedReasoningEffort sql.NullString
- inboundEndpoint sql.NullString
- upstreamEndpoint sql.NullString
- cacheTTLOverridden bool
- longContextBillingApplied bool
- channelID sql.NullInt64
- modelMappingChain sql.NullString
- billingTier sql.NullString
- billingMode sql.NullString
- accountStatsCost sql.NullFloat64
- upstreamRequestID sql.NullString
- sessionID sql.NullString
- nativeCompactionV2 bool
- createdAt time.Time
- )
- if err := scanner.Scan(
- &id,
- &userID,
- &apiKeyID,
- &accountID,
- &requestID,
- &model,
- &requestedModel,
- &upstreamModel,
- &upstreamResponseModel,
- &upstreamModelMismatch,
- &groupID,
- &subscriptionID,
- &inputTokens,
- &outputTokens,
- &cacheCreationTokens,
- &cacheReadTokens,
- &cacheCreation5m,
- &cacheCreation1h,
- &imageOutputTokens,
- &imageOutputCost,
- &imageInputTokens,
- &imageInputCost,
- &inputCost,
- &outputCost,
- &cacheCreationCost,
- &cacheReadCost,
- &totalCost,
- &actualCost,
- &rateMultiplier,
- &accountRateMultiplier,
- &billingType,
- &requestTypeRaw,
- &stream,
- &openaiWSMode,
- &durationMs,
- &firstTokenMs,
- &userAgent,
- &ipAddress,
- &imageCount,
- &imageSize,
- &imageInputSize,
- &imageOutputSize,
- &imageSizeSource,
- &imageSizeBreakdown,
- &videoCount,
- &videoResolution,
- &videoDurationSeconds,
- &serviceTier,
- &reasoningEffort,
- &requestedReasoningEffort,
- &inboundEndpoint,
- &upstreamEndpoint,
- &cacheTTLOverridden,
- &longContextBillingApplied,
- &channelID,
- &modelMappingChain,
- &billingTier,
- &billingMode,
- &accountStatsCost,
- &upstreamRequestID,
- &sessionID,
- &nativeCompactionV2,
- &createdAt,
- ); err != nil {
- return nil, err
- }
- log := &service.UsageLog{
- ID: id,
- UserID: userID,
- APIKeyID: apiKeyID,
- AccountID: accountID,
- Model: model,
- RequestedModel: coalesceTrimmedString(requestedModel, model),
- InputTokens: inputTokens,
- OutputTokens: outputTokens,
- CacheCreationTokens: cacheCreationTokens,
- CacheReadTokens: cacheReadTokens,
- CacheCreation5mTokens: cacheCreation5m,
- CacheCreation1hTokens: cacheCreation1h,
- ImageOutputTokens: imageOutputTokens,
- ImageOutputCost: imageOutputCost,
- ImageInputTokens: imageInputTokens,
- ImageInputCost: imageInputCost,
- InputCost: inputCost,
- OutputCost: outputCost,
- CacheCreationCost: cacheCreationCost,
- CacheReadCost: cacheReadCost,
- TotalCost: totalCost,
- ActualCost: actualCost,
- RateMultiplier: rateMultiplier,
- AccountRateMultiplier: nullFloat64Ptr(accountRateMultiplier),
- BillingType: int8(billingType),
- RequestType: service.RequestTypeFromInt16(requestTypeRaw),
- NativeCompactionV2: nativeCompactionV2,
- ImageCount: imageCount,
- VideoCount: videoCount,
- CacheTTLOverridden: cacheTTLOverridden,
- LongContextBillingApplied: longContextBillingApplied,
- CreatedAt: createdAt,
- }
- // 先回填 legacy 字段,再基于 legacy + request_type 计算最终请求类型,保证历史数据兼容。
- log.Stream = stream
- log.OpenAIWSMode = openaiWSMode
- log.RequestType = log.EffectiveRequestType()
- log.Stream, log.OpenAIWSMode = service.ApplyLegacyRequestFields(log.RequestType, stream, openaiWSMode)
- if requestID.Valid {
- log.RequestID = requestID.String
- }
- if groupID.Valid {
- value := groupID.Int64
- log.GroupID = &value
- }
- if subscriptionID.Valid {
- value := subscriptionID.Int64
- log.SubscriptionID = &value
- }
- if durationMs.Valid {
- value := int(durationMs.Int64)
- log.DurationMs = &value
- }
- if firstTokenMs.Valid {
- value := int(firstTokenMs.Int64)
- log.FirstTokenMs = &value
- }
- if userAgent.Valid {
- log.UserAgent = &userAgent.String
- }
- if ipAddress.Valid {
- log.IPAddress = &ipAddress.String
- }
- if imageSize.Valid {
- log.ImageSize = &imageSize.String
- }
- if imageInputSize.Valid {
- log.ImageInputSize = &imageInputSize.String
- }
- if imageOutputSize.Valid {
- log.ImageOutputSize = &imageOutputSize.String
- }
- if imageSizeSource.Valid {
- log.ImageSizeSource = &imageSizeSource.String
- }
- log.ImageSizeBreakdown = stringIntMapFromNullJSON(imageSizeBreakdown)
- if videoResolution.Valid {
- log.VideoResolution = &videoResolution.String
- }
- if videoDurationSeconds.Valid {
- value := int(videoDurationSeconds.Int64)
- log.VideoDurationSeconds = &value
- }
- if serviceTier.Valid {
- log.ServiceTier = &serviceTier.String
- }
- if reasoningEffort.Valid {
- log.ReasoningEffort = &reasoningEffort.String
- }
- if requestedReasoningEffort.Valid {
- log.RequestedReasoningEffort = &requestedReasoningEffort.String
- }
- if inboundEndpoint.Valid {
- log.InboundEndpoint = &inboundEndpoint.String
- }
- if upstreamEndpoint.Valid {
- log.UpstreamEndpoint = &upstreamEndpoint.String
- }
- if upstreamModel.Valid {
- log.UpstreamModel = &upstreamModel.String
- }
- if upstreamResponseModel.Valid {
- log.UpstreamResponseModel = &upstreamResponseModel.String
- }
- if upstreamModelMismatch.Valid {
- value := upstreamModelMismatch.Bool
- log.UpstreamModelMismatch = &value
- }
- if channelID.Valid {
- value := channelID.Int64
- log.ChannelID = &value
- }
- if modelMappingChain.Valid {
- log.ModelMappingChain = &modelMappingChain.String
- }
- if billingTier.Valid {
- log.BillingTier = &billingTier.String
- }
- if billingMode.Valid {
- log.BillingMode = &billingMode.String
- }
- if accountStatsCost.Valid {
- log.AccountStatsCost = &accountStatsCost.Float64
- }
- if sessionID.Valid {
- log.SessionID = &sessionID.String
- }
- if upstreamRequestID.Valid {
- log.UpstreamRequestID = &upstreamRequestID.String
- }
- return log, nil
- }
- func nullInt64(v *int64) sql.NullInt64 {
- if v == nil {
- return sql.NullInt64{}
- }
- return sql.NullInt64{Int64: *v, Valid: true}
- }
- func nullInt(v *int) sql.NullInt64 {
- if v == nil {
- return sql.NullInt64{}
- }
- return sql.NullInt64{Int64: int64(*v), Valid: true}
- }
- func nullFloat64Ptr(v sql.NullFloat64) *float64 {
- if !v.Valid {
- return nil
- }
- out := v.Float64
- return &out
- }
- func nullString(v *string) sql.NullString {
- if v == nil || *v == "" {
- return sql.NullString{}
- }
- return sql.NullString{String: *v, Valid: true}
- }
- func nullBool(v *bool) sql.NullBool {
- if v == nil {
- return sql.NullBool{}
- }
- return sql.NullBool{Bool: *v, Valid: true}
- }
- func nullStringIntMapJSON(v map[string]int) any {
- if len(v) == 0 {
- return nil
- }
- payload, err := json.Marshal(v)
- if err != nil {
- return nil
- }
- return string(payload)
- }
- func stringIntMapFromNullJSON(v sql.NullString) map[string]int {
- if !v.Valid || strings.TrimSpace(v.String) == "" {
- return nil
- }
- var out map[string]int
- if err := json.Unmarshal([]byte(v.String), &out); err != nil {
- return nil
- }
- if len(out) == 0 {
- return nil
- }
- return out
- }
- func coalesceTrimmedString(v sql.NullString, fallback string) string {
- if v.Valid && strings.TrimSpace(v.String) != "" {
- return v.String
- }
- return fallback
- }
- func setToSlice(set map[int64]struct{}) []int64 {
- out := make([]int64, 0, len(set))
- for id := range set {
- out = append(out, id)
- }
- return out
- }
|