package repository import ( "context" "database/sql" "errors" "fmt" "strings" "time" "billing/internal/pricing" ) const ( PriceSourceManual = "manual" PriceSourceModelsDev = "models.dev" ) // PriceSource records where the current effective local price was imported // from. It is bookkeeping only; billing always reads the embedded Policy. type PriceSource struct { Kind string CatalogID string Revision string FetchedAt time.Time } type PriceRecord struct { Policy pricing.Policy Source PriceSource } // ListPrices returns every configured exact-model policy. func (r *SQLiteUsageRepository) ListPrices(ctx context.Context) ([]pricing.Policy, error) { records, err := r.ListPriceRecords(ctx) if err != nil { return nil, err } policies := make([]pricing.Policy, 0, len(records)) for _, record := range records { policies = append(policies, record.Policy) } return policies, nil } // ListPriceRecords returns effective prices together with their optional // models.dev import link. func (r *SQLiteUsageRepository) ListPriceRecords(ctx context.Context) ([]PriceRecord, error) { rows, err := r.readDB.QueryContext(ctx, ` SELECT model, input_rate_micros, cache_read_rate_micros, cache_write_rate_micros, output_rate_micros, long_context_enabled, long_context_threshold, long_context_comparison, long_input_rate_micros, long_cache_read_rate_micros, long_cache_write_rate_micros, long_output_rate_micros, fast_pricing_enabled, fast_multiplier_numerator, fast_multiplier_denominator, source_kind, source_catalog_id, source_revision, source_fetched_at FROM model_prices ORDER BY model`) if err != nil { return nil, fmt.Errorf("查询模型价格: %w", err) } defer rows.Close() var records []PriceRecord for rows.Next() { var record PriceRecord var longEnabled bool var long pricing.LongContext var fetchedAt string if err := rows.Scan( &record.Policy.Model, &record.Policy.Base.InputMicrosPer1M, &record.Policy.Base.CacheReadMicrosPer1M, &record.Policy.Base.CacheWriteMicrosPer1M, &record.Policy.Base.OutputMicrosPer1M, &longEnabled, &long.ThresholdInputTokens, &long.Comparison, &long.Rates.InputMicrosPer1M, &long.Rates.CacheReadMicrosPer1M, &long.Rates.CacheWriteMicrosPer1M, &long.Rates.OutputMicrosPer1M, &record.Policy.FastPricingEnabled, &record.Policy.FastMultiplier.Numerator, &record.Policy.FastMultiplier.Denominator, &record.Source.Kind, &record.Source.CatalogID, &record.Source.Revision, &fetchedAt, ); err != nil { return nil, fmt.Errorf("读取模型价格: %w", err) } if longEnabled { record.Policy.LongContext = &long } if strings.TrimSpace(record.Source.Kind) == "" { record.Source.Kind = PriceSourceManual } if fetchedAt != "" { parsed, parseErr := time.Parse(time.RFC3339Nano, fetchedAt) if parseErr != nil { return nil, fmt.Errorf("解析模型价格来源时间: %w", parseErr) } record.Source.FetchedAt = parsed } records = append(records, record) } if err := rows.Err(); err != nil { return nil, fmt.Errorf("遍历模型价格: %w", err) } return records, nil } // UpsertPrice atomically replaces one model policy. func (r *SQLiteUsageRepository) UpsertPrice(ctx context.Context, policy pricing.Policy) error { return r.UpsertPriceRecord(ctx, PriceRecord{Policy: policy, Source: PriceSource{Kind: PriceSourceManual}}) } // UpsertPriceRecord atomically replaces one effective price and its source. func (r *SQLiteUsageRepository) UpsertPriceRecord(ctx context.Context, record PriceRecord) error { return r.UpsertPriceRecords(ctx, []PriceRecord{record}) } // UpsertPriceRecords updates a confirmed catalog diff in one transaction. func (r *SQLiteUsageRepository) UpsertPriceRecords(ctx context.Context, records []PriceRecord) error { if len(records) == 0 { return nil } for index := range records { if strings.TrimSpace(records[index].Source.Kind) == "" { records[index].Source.Kind = PriceSourceManual } if err := validatePriceRecord(records[index]); err != nil { return err } } tx, err := r.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("开始保存模型价格: %w", err) } for _, record := range records { var exists int if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM model_prices WHERE model=?`, record.Policy.Model).Scan(&exists); err != nil { _ = tx.Rollback() return fmt.Errorf("检查模型价格: %w", err) } if err := upsertPriceRecord(ctx, tx, record); err != nil { _ = tx.Rollback() return err } action := "新增" if exists > 0 { action = "修改" } if err := insertBusinessEvent(ctx, tx, "管理员"+action+"模型 `"+strings.ReplaceAll(record.Policy.Model, "`", "")+"` 的价格", BusinessEventSucceeded, time.Now().UTC()); err != nil { _ = tx.Rollback() return err } } if err := tx.Commit(); err != nil { return fmt.Errorf("提交模型价格: %w", err) } return nil } type priceExecer interface { ExecContext(context.Context, string, ...any) (sql.Result, error) } func upsertPriceRecord(ctx context.Context, executor priceExecer, record PriceRecord) error { policy := record.Policy longEnabled := policy.LongContext != nil long := pricing.LongContext{Comparison: "gt"} if policy.LongContext != nil { long = *policy.LongContext } fetchedAt := "" if !record.Source.FetchedAt.IsZero() { fetchedAt = record.Source.FetchedAt.UTC().Format(time.RFC3339Nano) } _, err := executor.ExecContext(ctx, ` INSERT INTO model_prices ( model, input_rate_micros, cache_read_rate_micros, cache_write_rate_micros, output_rate_micros, long_context_enabled, long_context_threshold, long_context_comparison, long_input_rate_micros, long_cache_read_rate_micros, long_cache_write_rate_micros, long_output_rate_micros, fast_pricing_enabled, fast_multiplier_numerator, fast_multiplier_denominator, source_kind, source_catalog_id, source_revision, source_fetched_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(model) DO UPDATE SET input_rate_micros=excluded.input_rate_micros, cache_read_rate_micros=excluded.cache_read_rate_micros, cache_write_rate_micros=excluded.cache_write_rate_micros, output_rate_micros=excluded.output_rate_micros, long_context_enabled=excluded.long_context_enabled, long_context_threshold=excluded.long_context_threshold, long_context_comparison=excluded.long_context_comparison, long_input_rate_micros=excluded.long_input_rate_micros, long_cache_read_rate_micros=excluded.long_cache_read_rate_micros, long_cache_write_rate_micros=excluded.long_cache_write_rate_micros, long_output_rate_micros=excluded.long_output_rate_micros, fast_pricing_enabled=excluded.fast_pricing_enabled, fast_multiplier_numerator=excluded.fast_multiplier_numerator, fast_multiplier_denominator=excluded.fast_multiplier_denominator, source_kind=excluded.source_kind, source_catalog_id=excluded.source_catalog_id, source_revision=excluded.source_revision, source_fetched_at=excluded.source_fetched_at, updated_at=excluded.updated_at`, policy.Model, policy.Base.InputMicrosPer1M, policy.Base.CacheReadMicrosPer1M, policy.Base.CacheWriteMicrosPer1M, policy.Base.OutputMicrosPer1M, longEnabled, long.ThresholdInputTokens, long.Comparison, long.Rates.InputMicrosPer1M, long.Rates.CacheReadMicrosPer1M, long.Rates.CacheWriteMicrosPer1M, long.Rates.OutputMicrosPer1M, policy.FastPricingEnabled, policy.FastMultiplier.Numerator, policy.FastMultiplier.Denominator, record.Source.Kind, record.Source.CatalogID, record.Source.Revision, fetchedAt, time.Now().UTC().Format(time.RFC3339Nano), ) if err != nil { return fmt.Errorf("保存模型价格: %w", err) } return nil } func validatePriceRecord(record PriceRecord) error { if err := record.Policy.Validate(); err != nil { return err } record.Source.Kind = strings.TrimSpace(record.Source.Kind) switch record.Source.Kind { case "", PriceSourceManual: if record.Source.CatalogID != "" || record.Source.Revision != "" || !record.Source.FetchedAt.IsZero() { return errors.New("手动价格不能包含目录来源") } case PriceSourceModelsDev: if strings.TrimSpace(record.Source.CatalogID) == "" || strings.TrimSpace(record.Source.Revision) == "" || record.Source.FetchedAt.IsZero() { return errors.New("models.dev 价格来源不完整") } default: return fmt.Errorf("不支持的价格来源 %q", record.Source.Kind) } return nil } const costBackfillBatchSize = 1000 // BackfillMissingCosts 只补算尚未定价的历史记录,已经保存的账单金额不会随价格修改而变化。 // 每批记录独立提交,避免百万级历史记录一次性占满内存或长期占用 writer。 func (r *SQLiteUsageRepository) BackfillMissingCosts(ctx context.Context, policy pricing.Policy) (int64, error) { if err := policy.Validate(); err != nil { return 0, err } type pendingCost struct { id int64 inputTokens int64 cacheReadTokens int64 cacheWriteTokens int64 outputTokens int64 serviceTier, speed string } var maxID int64 if err := r.readDB.QueryRowContext(ctx, ` SELECT COALESCE(MAX(id), 0) FROM usage_records WHERE model = ? COLLATE NOCASE AND cost_micros IS NULL`, policy.Model).Scan(&maxID); err != nil { return 0, fmt.Errorf("定位待补算用量范围: %w", err) } var updated int64 var lastID int64 for lastID < maxID { rows, err := r.readDB.QueryContext(ctx, ` SELECT id, input_tokens, cache_read_tokens, cache_write_tokens, output_tokens, service_tier, speed FROM usage_records WHERE model = ? COLLATE NOCASE AND cost_micros IS NULL AND id > ? AND id <= ? ORDER BY id LIMIT ?`, policy.Model, lastID, maxID, costBackfillBatchSize) if err != nil { return updated, fmt.Errorf("查询待补算用量: %w", err) } pending := make([]pendingCost, 0, costBackfillBatchSize) for rows.Next() { var item pendingCost if err := rows.Scan(&item.id, &item.inputTokens, &item.cacheReadTokens, &item.cacheWriteTokens, &item.outputTokens, &item.serviceTier, &item.speed); err != nil { _ = rows.Close() return updated, fmt.Errorf("读取待补算用量: %w", err) } pending = append(pending, item) } if err := rows.Err(); err != nil { _ = rows.Close() return updated, fmt.Errorf("遍历待补算用量: %w", err) } if err := rows.Close(); err != nil { return updated, fmt.Errorf("关闭待补算查询: %w", err) } if len(pending) == 0 { break } lastID = pending[len(pending)-1].id tx, err := r.db.BeginTx(ctx, nil) if err != nil { return updated, fmt.Errorf("开始补算事务: %w", err) } statement, err := tx.PrepareContext(ctx, ` UPDATE usage_records SET cost_micros = ?, price_tier = ?, fast_requested = ?, fast_pricing_applied = ?, price_multiplier_numerator = ?, price_multiplier_denominator = ? WHERE id = ? AND cost_micros IS NULL`) if err != nil { _ = tx.Rollback() return updated, fmt.Errorf("准备补算用量价格: %w", err) } for _, item := range pending { result, calculateErr := pricing.Calculate(policy, pricing.Usage{ InputTokens: item.inputTokens, CacheReadTokens: item.cacheReadTokens, CacheWriteTokens: item.cacheWriteTokens, OutputTokens: item.outputTokens, ServiceTier: item.serviceTier, Speed: item.speed, }) if calculateErr != nil { continue } change, updateErr := statement.ExecContext(ctx, result.CostMicros, result.PriceTier, result.FastRequested, result.FastApplied, result.MultiplierNumerator, result.MultiplierDenominator, item.id) if updateErr != nil { _ = statement.Close() _ = tx.Rollback() return updated, fmt.Errorf("补算用量价格: %w", updateErr) } count, countErr := change.RowsAffected() if countErr != nil { _ = statement.Close() _ = tx.Rollback() return updated, fmt.Errorf("读取补算数量: %w", countErr) } updated += count } if err := statement.Close(); err != nil { _ = tx.Rollback() return updated, fmt.Errorf("关闭补算语句: %w", err) } if err := tx.Commit(); err != nil { return updated, fmt.Errorf("提交补算事务: %w", err) } } return updated, nil } func (r *SQLiteUsageRepository) DeletePrice(ctx context.Context, model string) error { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("开始删除模型价格: %w", err) } defer func() { _ = tx.Rollback() }() result, err := tx.ExecContext(ctx, `DELETE FROM model_prices WHERE model = ?`, model) if err != nil { return fmt.Errorf("删除模型价格: %w", err) } if affected, _ := result.RowsAffected(); affected > 0 { event := "管理员删除模型 `" + strings.ReplaceAll(strings.TrimSpace(model), "`", "") + "` 的价格" if err := insertBusinessEvent(ctx, tx, event, BusinessEventSucceeded, time.Now().UTC()); err != nil { return err } } if err := tx.Commit(); err != nil { return fmt.Errorf("提交删除模型价格: %w", err) } return nil }