From fb99ceacc7ff33a90fd34197ceb6d2fb6fe75cf1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=A2=A8=E9=A2=9C?=
Date: Wed, 14 Jan 2026 16:12:08 +0800
Subject: [PATCH] =?UTF-8?q?feat(=E8=AE=A1=E8=B4=B9):=20=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E8=B4=A6=E5=8F=B7=E8=AE=A1=E8=B4=B9=E5=80=8D=E7=8E=87=E5=BF=AB?=
=?UTF-8?q?=E7=85=A7=E4=B8=8E=E7=BB=9F=E8=AE=A1=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 accounts.rate_multiplier(默认 1.0,允许 0)
- 使用 usage_logs.account_rate_multiplier 记录倍率快照,避免历史回算
- 统计/导出/管理端展示账号口径费用(total_cost * account_rate_multiplier)
---
backend/ent/account.go | 13 ++
backend/ent/account/account.go | 10 +
backend/ent/account/where.go | 45 ++++
backend/ent/account_create.go | 85 ++++++++
backend/ent/account_update.go | 54 +++++
backend/ent/migrate/schema.go | 44 ++--
backend/ent/mutation.go | 198 +++++++++++++++++-
backend/ent/runtime/runtime.go | 26 ++-
backend/ent/schema/account.go | 6 +
backend/ent/schema/usage_log.go | 6 +
backend/ent/usagelog.go | 16 +-
backend/ent/usagelog/usagelog.go | 8 +
backend/ent/usagelog/where.go | 55 +++++
backend/ent/usagelog_create.go | 98 +++++++++
backend/ent/usagelog_update.go | 72 +++++++
.../internal/handler/admin/account_handler.go | 19 ++
backend/internal/handler/dto/mappers.go | 2 +
backend/internal/handler/dto/types.go | 16 +-
.../internal/pkg/usagestats/account_stats.go | 12 +-
.../pkg/usagestats/usage_log_types.go | 31 +--
backend/internal/repository/account_repo.go | 16 ++
backend/internal/repository/usage_log_repo.go | 131 ++++++++----
.../usage_log_repo_integration_test.go | 76 ++++++-
backend/internal/server/api_contract_test.go | 31 +--
backend/internal/service/account.go | 37 +++-
.../account_billing_rate_multiplier_test.go | 27 +++
backend/internal/service/account_service.go | 17 +-
.../internal/service/account_usage_service.go | 28 ++-
backend/internal/service/admin_service.go | 48 +++--
backend/internal/service/gateway_service.go | 48 +++--
.../service/openai_gateway_service.go | 44 ++--
backend/internal/service/usage_log.go | 2 +
.../037_add_account_rate_multiplier.sql | 14 ++
frontend/src/api/admin/usage.ts | 1 +
.../components/account/AccountStatsModal.vue | 70 +++++--
.../account/AccountTodayStatsCell.vue | 13 +-
.../account/BulkEditAccountModal.vue | 41 +++-
.../components/account/CreateAccountModal.vue | 11 +-
.../components/account/EditAccountModal.vue | 9 +-
.../components/account/UsageProgressBar.vue | 15 +-
.../admin/account/AccountStatsModal.vue | 60 ++++--
.../admin/usage/UsageStatsCards.vue | 15 +-
.../src/components/admin/usage/UsageTable.vue | 39 ++--
frontend/src/i18n/locales/en.ts | 6 +
frontend/src/i18n/locales/zh.ts | 6 +
frontend/src/types/index.ts | 20 +-
frontend/src/views/admin/AccountsView.vue | 14 +-
frontend/src/views/admin/UsageView.vue | 4 +-
48 files changed, 1386 insertions(+), 273 deletions(-)
create mode 100644 backend/internal/service/account_billing_rate_multiplier_test.go
create mode 100644 backend/migrations/037_add_account_rate_multiplier.sql
diff --git a/backend/ent/account.go b/backend/ent/account.go
index e960d324..038aa7e5 100644
--- a/backend/ent/account.go
+++ b/backend/ent/account.go
@@ -43,6 +43,8 @@ type Account struct {
Concurrency int `json:"concurrency,omitempty"`
// Priority holds the value of the "priority" field.
Priority int `json:"priority,omitempty"`
+ // RateMultiplier holds the value of the "rate_multiplier" field.
+ RateMultiplier float64 `json:"rate_multiplier,omitempty"`
// Status holds the value of the "status" field.
Status string `json:"status,omitempty"`
// ErrorMessage holds the value of the "error_message" field.
@@ -135,6 +137,8 @@ func (*Account) scanValues(columns []string) ([]any, error) {
values[i] = new([]byte)
case account.FieldAutoPauseOnExpired, account.FieldSchedulable:
values[i] = new(sql.NullBool)
+ case account.FieldRateMultiplier:
+ values[i] = new(sql.NullFloat64)
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
values[i] = new(sql.NullInt64)
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldSessionWindowStatus:
@@ -241,6 +245,12 @@ func (_m *Account) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.Priority = int(value.Int64)
}
+ case account.FieldRateMultiplier:
+ if value, ok := values[i].(*sql.NullFloat64); !ok {
+ return fmt.Errorf("unexpected type %T for field rate_multiplier", values[i])
+ } else if value.Valid {
+ _m.RateMultiplier = value.Float64
+ }
case account.FieldStatus:
if value, ok := values[i].(*sql.NullString); !ok {
return fmt.Errorf("unexpected type %T for field status", values[i])
@@ -420,6 +430,9 @@ func (_m *Account) String() string {
builder.WriteString("priority=")
builder.WriteString(fmt.Sprintf("%v", _m.Priority))
builder.WriteString(", ")
+ builder.WriteString("rate_multiplier=")
+ builder.WriteString(fmt.Sprintf("%v", _m.RateMultiplier))
+ builder.WriteString(", ")
builder.WriteString("status=")
builder.WriteString(_m.Status)
builder.WriteString(", ")
diff --git a/backend/ent/account/account.go b/backend/ent/account/account.go
index 402e16ee..73c0e8c2 100644
--- a/backend/ent/account/account.go
+++ b/backend/ent/account/account.go
@@ -39,6 +39,8 @@ const (
FieldConcurrency = "concurrency"
// FieldPriority holds the string denoting the priority field in the database.
FieldPriority = "priority"
+ // FieldRateMultiplier holds the string denoting the rate_multiplier field in the database.
+ FieldRateMultiplier = "rate_multiplier"
// FieldStatus holds the string denoting the status field in the database.
FieldStatus = "status"
// FieldErrorMessage holds the string denoting the error_message field in the database.
@@ -116,6 +118,7 @@ var Columns = []string{
FieldProxyID,
FieldConcurrency,
FieldPriority,
+ FieldRateMultiplier,
FieldStatus,
FieldErrorMessage,
FieldLastUsedAt,
@@ -174,6 +177,8 @@ var (
DefaultConcurrency int
// DefaultPriority holds the default value on creation for the "priority" field.
DefaultPriority int
+ // DefaultRateMultiplier holds the default value on creation for the "rate_multiplier" field.
+ DefaultRateMultiplier float64
// DefaultStatus holds the default value on creation for the "status" field.
DefaultStatus string
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
@@ -244,6 +249,11 @@ func ByPriority(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldPriority, opts...).ToFunc()
}
+// ByRateMultiplier orders the results by the rate_multiplier field.
+func ByRateMultiplier(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldRateMultiplier, opts...).ToFunc()
+}
+
// ByStatus orders the results by the status field.
func ByStatus(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldStatus, opts...).ToFunc()
diff --git a/backend/ent/account/where.go b/backend/ent/account/where.go
index 6c639fd1..dea1127a 100644
--- a/backend/ent/account/where.go
+++ b/backend/ent/account/where.go
@@ -105,6 +105,11 @@ func Priority(v int) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldPriority, v))
}
+// RateMultiplier applies equality check predicate on the "rate_multiplier" field. It's identical to RateMultiplierEQ.
+func RateMultiplier(v float64) predicate.Account {
+ return predicate.Account(sql.FieldEQ(FieldRateMultiplier, v))
+}
+
// Status applies equality check predicate on the "status" field. It's identical to StatusEQ.
func Status(v string) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldStatus, v))
@@ -675,6 +680,46 @@ func PriorityLTE(v int) predicate.Account {
return predicate.Account(sql.FieldLTE(FieldPriority, v))
}
+// RateMultiplierEQ applies the EQ predicate on the "rate_multiplier" field.
+func RateMultiplierEQ(v float64) predicate.Account {
+ return predicate.Account(sql.FieldEQ(FieldRateMultiplier, v))
+}
+
+// RateMultiplierNEQ applies the NEQ predicate on the "rate_multiplier" field.
+func RateMultiplierNEQ(v float64) predicate.Account {
+ return predicate.Account(sql.FieldNEQ(FieldRateMultiplier, v))
+}
+
+// RateMultiplierIn applies the In predicate on the "rate_multiplier" field.
+func RateMultiplierIn(vs ...float64) predicate.Account {
+ return predicate.Account(sql.FieldIn(FieldRateMultiplier, vs...))
+}
+
+// RateMultiplierNotIn applies the NotIn predicate on the "rate_multiplier" field.
+func RateMultiplierNotIn(vs ...float64) predicate.Account {
+ return predicate.Account(sql.FieldNotIn(FieldRateMultiplier, vs...))
+}
+
+// RateMultiplierGT applies the GT predicate on the "rate_multiplier" field.
+func RateMultiplierGT(v float64) predicate.Account {
+ return predicate.Account(sql.FieldGT(FieldRateMultiplier, v))
+}
+
+// RateMultiplierGTE applies the GTE predicate on the "rate_multiplier" field.
+func RateMultiplierGTE(v float64) predicate.Account {
+ return predicate.Account(sql.FieldGTE(FieldRateMultiplier, v))
+}
+
+// RateMultiplierLT applies the LT predicate on the "rate_multiplier" field.
+func RateMultiplierLT(v float64) predicate.Account {
+ return predicate.Account(sql.FieldLT(FieldRateMultiplier, v))
+}
+
+// RateMultiplierLTE applies the LTE predicate on the "rate_multiplier" field.
+func RateMultiplierLTE(v float64) predicate.Account {
+ return predicate.Account(sql.FieldLTE(FieldRateMultiplier, v))
+}
+
// StatusEQ applies the EQ predicate on the "status" field.
func StatusEQ(v string) predicate.Account {
return predicate.Account(sql.FieldEQ(FieldStatus, v))
diff --git a/backend/ent/account_create.go b/backend/ent/account_create.go
index 0725d43d..42a561cf 100644
--- a/backend/ent/account_create.go
+++ b/backend/ent/account_create.go
@@ -153,6 +153,20 @@ func (_c *AccountCreate) SetNillablePriority(v *int) *AccountCreate {
return _c
}
+// SetRateMultiplier sets the "rate_multiplier" field.
+func (_c *AccountCreate) SetRateMultiplier(v float64) *AccountCreate {
+ _c.mutation.SetRateMultiplier(v)
+ return _c
+}
+
+// SetNillableRateMultiplier sets the "rate_multiplier" field if the given value is not nil.
+func (_c *AccountCreate) SetNillableRateMultiplier(v *float64) *AccountCreate {
+ if v != nil {
+ _c.SetRateMultiplier(*v)
+ }
+ return _c
+}
+
// SetStatus sets the "status" field.
func (_c *AccountCreate) SetStatus(v string) *AccountCreate {
_c.mutation.SetStatus(v)
@@ -429,6 +443,10 @@ func (_c *AccountCreate) defaults() error {
v := account.DefaultPriority
_c.mutation.SetPriority(v)
}
+ if _, ok := _c.mutation.RateMultiplier(); !ok {
+ v := account.DefaultRateMultiplier
+ _c.mutation.SetRateMultiplier(v)
+ }
if _, ok := _c.mutation.Status(); !ok {
v := account.DefaultStatus
_c.mutation.SetStatus(v)
@@ -488,6 +506,9 @@ func (_c *AccountCreate) check() error {
if _, ok := _c.mutation.Priority(); !ok {
return &ValidationError{Name: "priority", err: errors.New(`ent: missing required field "Account.priority"`)}
}
+ if _, ok := _c.mutation.RateMultiplier(); !ok {
+ return &ValidationError{Name: "rate_multiplier", err: errors.New(`ent: missing required field "Account.rate_multiplier"`)}
+ }
if _, ok := _c.mutation.Status(); !ok {
return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "Account.status"`)}
}
@@ -578,6 +599,10 @@ func (_c *AccountCreate) createSpec() (*Account, *sqlgraph.CreateSpec) {
_spec.SetField(account.FieldPriority, field.TypeInt, value)
_node.Priority = value
}
+ if value, ok := _c.mutation.RateMultiplier(); ok {
+ _spec.SetField(account.FieldRateMultiplier, field.TypeFloat64, value)
+ _node.RateMultiplier = value
+ }
if value, ok := _c.mutation.Status(); ok {
_spec.SetField(account.FieldStatus, field.TypeString, value)
_node.Status = value
@@ -893,6 +918,24 @@ func (u *AccountUpsert) AddPriority(v int) *AccountUpsert {
return u
}
+// SetRateMultiplier sets the "rate_multiplier" field.
+func (u *AccountUpsert) SetRateMultiplier(v float64) *AccountUpsert {
+ u.Set(account.FieldRateMultiplier, v)
+ return u
+}
+
+// UpdateRateMultiplier sets the "rate_multiplier" field to the value that was provided on create.
+func (u *AccountUpsert) UpdateRateMultiplier() *AccountUpsert {
+ u.SetExcluded(account.FieldRateMultiplier)
+ return u
+}
+
+// AddRateMultiplier adds v to the "rate_multiplier" field.
+func (u *AccountUpsert) AddRateMultiplier(v float64) *AccountUpsert {
+ u.Add(account.FieldRateMultiplier, v)
+ return u
+}
+
// SetStatus sets the "status" field.
func (u *AccountUpsert) SetStatus(v string) *AccountUpsert {
u.Set(account.FieldStatus, v)
@@ -1325,6 +1368,27 @@ func (u *AccountUpsertOne) UpdatePriority() *AccountUpsertOne {
})
}
+// SetRateMultiplier sets the "rate_multiplier" field.
+func (u *AccountUpsertOne) SetRateMultiplier(v float64) *AccountUpsertOne {
+ return u.Update(func(s *AccountUpsert) {
+ s.SetRateMultiplier(v)
+ })
+}
+
+// AddRateMultiplier adds v to the "rate_multiplier" field.
+func (u *AccountUpsertOne) AddRateMultiplier(v float64) *AccountUpsertOne {
+ return u.Update(func(s *AccountUpsert) {
+ s.AddRateMultiplier(v)
+ })
+}
+
+// UpdateRateMultiplier sets the "rate_multiplier" field to the value that was provided on create.
+func (u *AccountUpsertOne) UpdateRateMultiplier() *AccountUpsertOne {
+ return u.Update(func(s *AccountUpsert) {
+ s.UpdateRateMultiplier()
+ })
+}
+
// SetStatus sets the "status" field.
func (u *AccountUpsertOne) SetStatus(v string) *AccountUpsertOne {
return u.Update(func(s *AccountUpsert) {
@@ -1956,6 +2020,27 @@ func (u *AccountUpsertBulk) UpdatePriority() *AccountUpsertBulk {
})
}
+// SetRateMultiplier sets the "rate_multiplier" field.
+func (u *AccountUpsertBulk) SetRateMultiplier(v float64) *AccountUpsertBulk {
+ return u.Update(func(s *AccountUpsert) {
+ s.SetRateMultiplier(v)
+ })
+}
+
+// AddRateMultiplier adds v to the "rate_multiplier" field.
+func (u *AccountUpsertBulk) AddRateMultiplier(v float64) *AccountUpsertBulk {
+ return u.Update(func(s *AccountUpsert) {
+ s.AddRateMultiplier(v)
+ })
+}
+
+// UpdateRateMultiplier sets the "rate_multiplier" field to the value that was provided on create.
+func (u *AccountUpsertBulk) UpdateRateMultiplier() *AccountUpsertBulk {
+ return u.Update(func(s *AccountUpsert) {
+ s.UpdateRateMultiplier()
+ })
+}
+
// SetStatus sets the "status" field.
func (u *AccountUpsertBulk) SetStatus(v string) *AccountUpsertBulk {
return u.Update(func(s *AccountUpsert) {
diff --git a/backend/ent/account_update.go b/backend/ent/account_update.go
index dcc3212d..63fab096 100644
--- a/backend/ent/account_update.go
+++ b/backend/ent/account_update.go
@@ -193,6 +193,27 @@ func (_u *AccountUpdate) AddPriority(v int) *AccountUpdate {
return _u
}
+// SetRateMultiplier sets the "rate_multiplier" field.
+func (_u *AccountUpdate) SetRateMultiplier(v float64) *AccountUpdate {
+ _u.mutation.ResetRateMultiplier()
+ _u.mutation.SetRateMultiplier(v)
+ return _u
+}
+
+// SetNillableRateMultiplier sets the "rate_multiplier" field if the given value is not nil.
+func (_u *AccountUpdate) SetNillableRateMultiplier(v *float64) *AccountUpdate {
+ if v != nil {
+ _u.SetRateMultiplier(*v)
+ }
+ return _u
+}
+
+// AddRateMultiplier adds value to the "rate_multiplier" field.
+func (_u *AccountUpdate) AddRateMultiplier(v float64) *AccountUpdate {
+ _u.mutation.AddRateMultiplier(v)
+ return _u
+}
+
// SetStatus sets the "status" field.
func (_u *AccountUpdate) SetStatus(v string) *AccountUpdate {
_u.mutation.SetStatus(v)
@@ -629,6 +650,12 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.AddedPriority(); ok {
_spec.AddField(account.FieldPriority, field.TypeInt, value)
}
+ if value, ok := _u.mutation.RateMultiplier(); ok {
+ _spec.SetField(account.FieldRateMultiplier, field.TypeFloat64, value)
+ }
+ if value, ok := _u.mutation.AddedRateMultiplier(); ok {
+ _spec.AddField(account.FieldRateMultiplier, field.TypeFloat64, value)
+ }
if value, ok := _u.mutation.Status(); ok {
_spec.SetField(account.FieldStatus, field.TypeString, value)
}
@@ -1005,6 +1032,27 @@ func (_u *AccountUpdateOne) AddPriority(v int) *AccountUpdateOne {
return _u
}
+// SetRateMultiplier sets the "rate_multiplier" field.
+func (_u *AccountUpdateOne) SetRateMultiplier(v float64) *AccountUpdateOne {
+ _u.mutation.ResetRateMultiplier()
+ _u.mutation.SetRateMultiplier(v)
+ return _u
+}
+
+// SetNillableRateMultiplier sets the "rate_multiplier" field if the given value is not nil.
+func (_u *AccountUpdateOne) SetNillableRateMultiplier(v *float64) *AccountUpdateOne {
+ if v != nil {
+ _u.SetRateMultiplier(*v)
+ }
+ return _u
+}
+
+// AddRateMultiplier adds value to the "rate_multiplier" field.
+func (_u *AccountUpdateOne) AddRateMultiplier(v float64) *AccountUpdateOne {
+ _u.mutation.AddRateMultiplier(v)
+ return _u
+}
+
// SetStatus sets the "status" field.
func (_u *AccountUpdateOne) SetStatus(v string) *AccountUpdateOne {
_u.mutation.SetStatus(v)
@@ -1471,6 +1519,12 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er
if value, ok := _u.mutation.AddedPriority(); ok {
_spec.AddField(account.FieldPriority, field.TypeInt, value)
}
+ if value, ok := _u.mutation.RateMultiplier(); ok {
+ _spec.SetField(account.FieldRateMultiplier, field.TypeFloat64, value)
+ }
+ if value, ok := _u.mutation.AddedRateMultiplier(); ok {
+ _spec.AddField(account.FieldRateMultiplier, field.TypeFloat64, value)
+ }
if value, ok := _u.mutation.Status(); ok {
_spec.SetField(account.FieldStatus, field.TypeString, value)
}
diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go
index 41cd8b01..d769f611 100644
--- a/backend/ent/migrate/schema.go
+++ b/backend/ent/migrate/schema.go
@@ -79,6 +79,7 @@ var (
{Name: "extra", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "concurrency", Type: field.TypeInt, Default: 3},
{Name: "priority", Type: field.TypeInt, Default: 50},
+ {Name: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
{Name: "error_message", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
{Name: "last_used_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
@@ -101,7 +102,7 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "accounts_proxies_proxy",
- Columns: []*schema.Column{AccountsColumns[24]},
+ Columns: []*schema.Column{AccountsColumns[25]},
RefColumns: []*schema.Column{ProxiesColumns[0]},
OnDelete: schema.SetNull,
},
@@ -120,12 +121,12 @@ var (
{
Name: "account_status",
Unique: false,
- Columns: []*schema.Column{AccountsColumns[12]},
+ Columns: []*schema.Column{AccountsColumns[13]},
},
{
Name: "account_proxy_id",
Unique: false,
- Columns: []*schema.Column{AccountsColumns[24]},
+ Columns: []*schema.Column{AccountsColumns[25]},
},
{
Name: "account_priority",
@@ -135,27 +136,27 @@ var (
{
Name: "account_last_used_at",
Unique: false,
- Columns: []*schema.Column{AccountsColumns[14]},
+ Columns: []*schema.Column{AccountsColumns[15]},
},
{
Name: "account_schedulable",
Unique: false,
- Columns: []*schema.Column{AccountsColumns[17]},
+ Columns: []*schema.Column{AccountsColumns[18]},
},
{
Name: "account_rate_limited_at",
Unique: false,
- Columns: []*schema.Column{AccountsColumns[18]},
+ Columns: []*schema.Column{AccountsColumns[19]},
},
{
Name: "account_rate_limit_reset_at",
Unique: false,
- Columns: []*schema.Column{AccountsColumns[19]},
+ Columns: []*schema.Column{AccountsColumns[20]},
},
{
Name: "account_overload_until",
Unique: false,
- Columns: []*schema.Column{AccountsColumns[20]},
+ Columns: []*schema.Column{AccountsColumns[21]},
},
{
Name: "account_deleted_at",
@@ -449,6 +450,7 @@ var (
{Name: "total_cost", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,10)"}},
{Name: "actual_cost", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,10)"}},
{Name: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
+ {Name: "account_rate_multiplier", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(10,4)"}},
{Name: "billing_type", Type: field.TypeInt8, Default: 0},
{Name: "stream", Type: field.TypeBool, Default: false},
{Name: "duration_ms", Type: field.TypeInt, Nullable: true},
@@ -472,31 +474,31 @@ var (
ForeignKeys: []*schema.ForeignKey{
{
Symbol: "usage_logs_api_keys_usage_logs",
- Columns: []*schema.Column{UsageLogsColumns[25]},
+ Columns: []*schema.Column{UsageLogsColumns[26]},
RefColumns: []*schema.Column{APIKeysColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "usage_logs_accounts_usage_logs",
- Columns: []*schema.Column{UsageLogsColumns[26]},
+ Columns: []*schema.Column{UsageLogsColumns[27]},
RefColumns: []*schema.Column{AccountsColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "usage_logs_groups_usage_logs",
- Columns: []*schema.Column{UsageLogsColumns[27]},
+ Columns: []*schema.Column{UsageLogsColumns[28]},
RefColumns: []*schema.Column{GroupsColumns[0]},
OnDelete: schema.SetNull,
},
{
Symbol: "usage_logs_users_usage_logs",
- Columns: []*schema.Column{UsageLogsColumns[28]},
+ Columns: []*schema.Column{UsageLogsColumns[29]},
RefColumns: []*schema.Column{UsersColumns[0]},
OnDelete: schema.NoAction,
},
{
Symbol: "usage_logs_user_subscriptions_usage_logs",
- Columns: []*schema.Column{UsageLogsColumns[29]},
+ Columns: []*schema.Column{UsageLogsColumns[30]},
RefColumns: []*schema.Column{UserSubscriptionsColumns[0]},
OnDelete: schema.SetNull,
},
@@ -505,32 +507,32 @@ var (
{
Name: "usagelog_user_id",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[28]},
+ Columns: []*schema.Column{UsageLogsColumns[29]},
},
{
Name: "usagelog_api_key_id",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[25]},
+ Columns: []*schema.Column{UsageLogsColumns[26]},
},
{
Name: "usagelog_account_id",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[26]},
+ Columns: []*schema.Column{UsageLogsColumns[27]},
},
{
Name: "usagelog_group_id",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[27]},
+ Columns: []*schema.Column{UsageLogsColumns[28]},
},
{
Name: "usagelog_subscription_id",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[29]},
+ Columns: []*schema.Column{UsageLogsColumns[30]},
},
{
Name: "usagelog_created_at",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[24]},
+ Columns: []*schema.Column{UsageLogsColumns[25]},
},
{
Name: "usagelog_model",
@@ -545,12 +547,12 @@ var (
{
Name: "usagelog_user_id_created_at",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[28], UsageLogsColumns[24]},
+ Columns: []*schema.Column{UsageLogsColumns[29], UsageLogsColumns[25]},
},
{
Name: "usagelog_api_key_id_created_at",
Unique: false,
- Columns: []*schema.Column{UsageLogsColumns[25], UsageLogsColumns[24]},
+ Columns: []*schema.Column{UsageLogsColumns[26], UsageLogsColumns[25]},
},
},
}
diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go
index 732abd1c..3509efed 100644
--- a/backend/ent/mutation.go
+++ b/backend/ent/mutation.go
@@ -1187,6 +1187,8 @@ type AccountMutation struct {
addconcurrency *int
priority *int
addpriority *int
+ rate_multiplier *float64
+ addrate_multiplier *float64
status *string
error_message *string
last_used_at *time.Time
@@ -1822,6 +1824,62 @@ func (m *AccountMutation) ResetPriority() {
m.addpriority = nil
}
+// SetRateMultiplier sets the "rate_multiplier" field.
+func (m *AccountMutation) SetRateMultiplier(f float64) {
+ m.rate_multiplier = &f
+ m.addrate_multiplier = nil
+}
+
+// RateMultiplier returns the value of the "rate_multiplier" field in the mutation.
+func (m *AccountMutation) RateMultiplier() (r float64, exists bool) {
+ v := m.rate_multiplier
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldRateMultiplier returns the old "rate_multiplier" field's value of the Account entity.
+// If the Account object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *AccountMutation) OldRateMultiplier(ctx context.Context) (v float64, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldRateMultiplier is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldRateMultiplier requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldRateMultiplier: %w", err)
+ }
+ return oldValue.RateMultiplier, nil
+}
+
+// AddRateMultiplier adds f to the "rate_multiplier" field.
+func (m *AccountMutation) AddRateMultiplier(f float64) {
+ if m.addrate_multiplier != nil {
+ *m.addrate_multiplier += f
+ } else {
+ m.addrate_multiplier = &f
+ }
+}
+
+// AddedRateMultiplier returns the value that was added to the "rate_multiplier" field in this mutation.
+func (m *AccountMutation) AddedRateMultiplier() (r float64, exists bool) {
+ v := m.addrate_multiplier
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// ResetRateMultiplier resets all changes to the "rate_multiplier" field.
+func (m *AccountMutation) ResetRateMultiplier() {
+ m.rate_multiplier = nil
+ m.addrate_multiplier = nil
+}
+
// SetStatus sets the "status" field.
func (m *AccountMutation) SetStatus(s string) {
m.status = &s
@@ -2540,7 +2598,7 @@ func (m *AccountMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *AccountMutation) Fields() []string {
- fields := make([]string, 0, 24)
+ fields := make([]string, 0, 25)
if m.created_at != nil {
fields = append(fields, account.FieldCreatedAt)
}
@@ -2577,6 +2635,9 @@ func (m *AccountMutation) Fields() []string {
if m.priority != nil {
fields = append(fields, account.FieldPriority)
}
+ if m.rate_multiplier != nil {
+ fields = append(fields, account.FieldRateMultiplier)
+ }
if m.status != nil {
fields = append(fields, account.FieldStatus)
}
@@ -2645,6 +2706,8 @@ func (m *AccountMutation) Field(name string) (ent.Value, bool) {
return m.Concurrency()
case account.FieldPriority:
return m.Priority()
+ case account.FieldRateMultiplier:
+ return m.RateMultiplier()
case account.FieldStatus:
return m.Status()
case account.FieldErrorMessage:
@@ -2702,6 +2765,8 @@ func (m *AccountMutation) OldField(ctx context.Context, name string) (ent.Value,
return m.OldConcurrency(ctx)
case account.FieldPriority:
return m.OldPriority(ctx)
+ case account.FieldRateMultiplier:
+ return m.OldRateMultiplier(ctx)
case account.FieldStatus:
return m.OldStatus(ctx)
case account.FieldErrorMessage:
@@ -2819,6 +2884,13 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error {
}
m.SetPriority(v)
return nil
+ case account.FieldRateMultiplier:
+ v, ok := value.(float64)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetRateMultiplier(v)
+ return nil
case account.FieldStatus:
v, ok := value.(string)
if !ok {
@@ -2917,6 +2989,9 @@ func (m *AccountMutation) AddedFields() []string {
if m.addpriority != nil {
fields = append(fields, account.FieldPriority)
}
+ if m.addrate_multiplier != nil {
+ fields = append(fields, account.FieldRateMultiplier)
+ }
return fields
}
@@ -2929,6 +3004,8 @@ func (m *AccountMutation) AddedField(name string) (ent.Value, bool) {
return m.AddedConcurrency()
case account.FieldPriority:
return m.AddedPriority()
+ case account.FieldRateMultiplier:
+ return m.AddedRateMultiplier()
}
return nil, false
}
@@ -2952,6 +3029,13 @@ func (m *AccountMutation) AddField(name string, value ent.Value) error {
}
m.AddPriority(v)
return nil
+ case account.FieldRateMultiplier:
+ v, ok := value.(float64)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.AddRateMultiplier(v)
+ return nil
}
return fmt.Errorf("unknown Account numeric field %s", name)
}
@@ -3090,6 +3174,9 @@ func (m *AccountMutation) ResetField(name string) error {
case account.FieldPriority:
m.ResetPriority()
return nil
+ case account.FieldRateMultiplier:
+ m.ResetRateMultiplier()
+ return nil
case account.FieldStatus:
m.ResetStatus()
return nil
@@ -10190,6 +10277,8 @@ type UsageLogMutation struct {
addactual_cost *float64
rate_multiplier *float64
addrate_multiplier *float64
+ account_rate_multiplier *float64
+ addaccount_rate_multiplier *float64
billing_type *int8
addbilling_type *int8
stream *bool
@@ -11323,6 +11412,76 @@ func (m *UsageLogMutation) ResetRateMultiplier() {
m.addrate_multiplier = nil
}
+// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
+func (m *UsageLogMutation) SetAccountRateMultiplier(f float64) {
+ m.account_rate_multiplier = &f
+ m.addaccount_rate_multiplier = nil
+}
+
+// AccountRateMultiplier returns the value of the "account_rate_multiplier" field in the mutation.
+func (m *UsageLogMutation) AccountRateMultiplier() (r float64, exists bool) {
+ v := m.account_rate_multiplier
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// OldAccountRateMultiplier returns the old "account_rate_multiplier" field's value of the UsageLog entity.
+// If the UsageLog object wasn't provided to the builder, the object is fetched from the database.
+// An error is returned if the mutation operation is not UpdateOne, or the database query fails.
+func (m *UsageLogMutation) OldAccountRateMultiplier(ctx context.Context) (v *float64, err error) {
+ if !m.op.Is(OpUpdateOne) {
+ return v, errors.New("OldAccountRateMultiplier is only allowed on UpdateOne operations")
+ }
+ if m.id == nil || m.oldValue == nil {
+ return v, errors.New("OldAccountRateMultiplier requires an ID field in the mutation")
+ }
+ oldValue, err := m.oldValue(ctx)
+ if err != nil {
+ return v, fmt.Errorf("querying old value for OldAccountRateMultiplier: %w", err)
+ }
+ return oldValue.AccountRateMultiplier, nil
+}
+
+// AddAccountRateMultiplier adds f to the "account_rate_multiplier" field.
+func (m *UsageLogMutation) AddAccountRateMultiplier(f float64) {
+ if m.addaccount_rate_multiplier != nil {
+ *m.addaccount_rate_multiplier += f
+ } else {
+ m.addaccount_rate_multiplier = &f
+ }
+}
+
+// AddedAccountRateMultiplier returns the value that was added to the "account_rate_multiplier" field in this mutation.
+func (m *UsageLogMutation) AddedAccountRateMultiplier() (r float64, exists bool) {
+ v := m.addaccount_rate_multiplier
+ if v == nil {
+ return
+ }
+ return *v, true
+}
+
+// ClearAccountRateMultiplier clears the value of the "account_rate_multiplier" field.
+func (m *UsageLogMutation) ClearAccountRateMultiplier() {
+ m.account_rate_multiplier = nil
+ m.addaccount_rate_multiplier = nil
+ m.clearedFields[usagelog.FieldAccountRateMultiplier] = struct{}{}
+}
+
+// AccountRateMultiplierCleared returns if the "account_rate_multiplier" field was cleared in this mutation.
+func (m *UsageLogMutation) AccountRateMultiplierCleared() bool {
+ _, ok := m.clearedFields[usagelog.FieldAccountRateMultiplier]
+ return ok
+}
+
+// ResetAccountRateMultiplier resets all changes to the "account_rate_multiplier" field.
+func (m *UsageLogMutation) ResetAccountRateMultiplier() {
+ m.account_rate_multiplier = nil
+ m.addaccount_rate_multiplier = nil
+ delete(m.clearedFields, usagelog.FieldAccountRateMultiplier)
+}
+
// SetBillingType sets the "billing_type" field.
func (m *UsageLogMutation) SetBillingType(i int8) {
m.billing_type = &i
@@ -11963,7 +12122,7 @@ func (m *UsageLogMutation) Type() string {
// order to get all numeric fields that were incremented/decremented, call
// AddedFields().
func (m *UsageLogMutation) Fields() []string {
- fields := make([]string, 0, 29)
+ fields := make([]string, 0, 30)
if m.user != nil {
fields = append(fields, usagelog.FieldUserID)
}
@@ -12024,6 +12183,9 @@ func (m *UsageLogMutation) Fields() []string {
if m.rate_multiplier != nil {
fields = append(fields, usagelog.FieldRateMultiplier)
}
+ if m.account_rate_multiplier != nil {
+ fields = append(fields, usagelog.FieldAccountRateMultiplier)
+ }
if m.billing_type != nil {
fields = append(fields, usagelog.FieldBillingType)
}
@@ -12099,6 +12261,8 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) {
return m.ActualCost()
case usagelog.FieldRateMultiplier:
return m.RateMultiplier()
+ case usagelog.FieldAccountRateMultiplier:
+ return m.AccountRateMultiplier()
case usagelog.FieldBillingType:
return m.BillingType()
case usagelog.FieldStream:
@@ -12166,6 +12330,8 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value
return m.OldActualCost(ctx)
case usagelog.FieldRateMultiplier:
return m.OldRateMultiplier(ctx)
+ case usagelog.FieldAccountRateMultiplier:
+ return m.OldAccountRateMultiplier(ctx)
case usagelog.FieldBillingType:
return m.OldBillingType(ctx)
case usagelog.FieldStream:
@@ -12333,6 +12499,13 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error {
}
m.SetRateMultiplier(v)
return nil
+ case usagelog.FieldAccountRateMultiplier:
+ v, ok := value.(float64)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.SetAccountRateMultiplier(v)
+ return nil
case usagelog.FieldBillingType:
v, ok := value.(int8)
if !ok {
@@ -12443,6 +12616,9 @@ func (m *UsageLogMutation) AddedFields() []string {
if m.addrate_multiplier != nil {
fields = append(fields, usagelog.FieldRateMultiplier)
}
+ if m.addaccount_rate_multiplier != nil {
+ fields = append(fields, usagelog.FieldAccountRateMultiplier)
+ }
if m.addbilling_type != nil {
fields = append(fields, usagelog.FieldBillingType)
}
@@ -12489,6 +12665,8 @@ func (m *UsageLogMutation) AddedField(name string) (ent.Value, bool) {
return m.AddedActualCost()
case usagelog.FieldRateMultiplier:
return m.AddedRateMultiplier()
+ case usagelog.FieldAccountRateMultiplier:
+ return m.AddedAccountRateMultiplier()
case usagelog.FieldBillingType:
return m.AddedBillingType()
case usagelog.FieldDurationMs:
@@ -12597,6 +12775,13 @@ func (m *UsageLogMutation) AddField(name string, value ent.Value) error {
}
m.AddRateMultiplier(v)
return nil
+ case usagelog.FieldAccountRateMultiplier:
+ v, ok := value.(float64)
+ if !ok {
+ return fmt.Errorf("unexpected type %T for field %s", value, name)
+ }
+ m.AddAccountRateMultiplier(v)
+ return nil
case usagelog.FieldBillingType:
v, ok := value.(int8)
if !ok {
@@ -12639,6 +12824,9 @@ func (m *UsageLogMutation) ClearedFields() []string {
if m.FieldCleared(usagelog.FieldSubscriptionID) {
fields = append(fields, usagelog.FieldSubscriptionID)
}
+ if m.FieldCleared(usagelog.FieldAccountRateMultiplier) {
+ fields = append(fields, usagelog.FieldAccountRateMultiplier)
+ }
if m.FieldCleared(usagelog.FieldDurationMs) {
fields = append(fields, usagelog.FieldDurationMs)
}
@@ -12674,6 +12862,9 @@ func (m *UsageLogMutation) ClearField(name string) error {
case usagelog.FieldSubscriptionID:
m.ClearSubscriptionID()
return nil
+ case usagelog.FieldAccountRateMultiplier:
+ m.ClearAccountRateMultiplier()
+ return nil
case usagelog.FieldDurationMs:
m.ClearDurationMs()
return nil
@@ -12757,6 +12948,9 @@ func (m *UsageLogMutation) ResetField(name string) error {
case usagelog.FieldRateMultiplier:
m.ResetRateMultiplier()
return nil
+ case usagelog.FieldAccountRateMultiplier:
+ m.ResetAccountRateMultiplier()
+ return nil
case usagelog.FieldBillingType:
m.ResetBillingType()
return nil
diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go
index ad1aa626..ed13c852 100644
--- a/backend/ent/runtime/runtime.go
+++ b/backend/ent/runtime/runtime.go
@@ -177,22 +177,26 @@ func init() {
accountDescPriority := accountFields[8].Descriptor()
// account.DefaultPriority holds the default value on creation for the priority field.
account.DefaultPriority = accountDescPriority.Default.(int)
+ // accountDescRateMultiplier is the schema descriptor for rate_multiplier field.
+ accountDescRateMultiplier := accountFields[9].Descriptor()
+ // account.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field.
+ account.DefaultRateMultiplier = accountDescRateMultiplier.Default.(float64)
// accountDescStatus is the schema descriptor for status field.
- accountDescStatus := accountFields[9].Descriptor()
+ accountDescStatus := accountFields[10].Descriptor()
// account.DefaultStatus holds the default value on creation for the status field.
account.DefaultStatus = accountDescStatus.Default.(string)
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
account.StatusValidator = accountDescStatus.Validators[0].(func(string) error)
// accountDescAutoPauseOnExpired is the schema descriptor for auto_pause_on_expired field.
- accountDescAutoPauseOnExpired := accountFields[13].Descriptor()
+ accountDescAutoPauseOnExpired := accountFields[14].Descriptor()
// account.DefaultAutoPauseOnExpired holds the default value on creation for the auto_pause_on_expired field.
account.DefaultAutoPauseOnExpired = accountDescAutoPauseOnExpired.Default.(bool)
// accountDescSchedulable is the schema descriptor for schedulable field.
- accountDescSchedulable := accountFields[14].Descriptor()
+ accountDescSchedulable := accountFields[15].Descriptor()
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
- accountDescSessionWindowStatus := accountFields[20].Descriptor()
+ accountDescSessionWindowStatus := accountFields[21].Descriptor()
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error)
accountgroupFields := schema.AccountGroup{}.Fields()
@@ -578,31 +582,31 @@ func init() {
// usagelog.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field.
usagelog.DefaultRateMultiplier = usagelogDescRateMultiplier.Default.(float64)
// usagelogDescBillingType is the schema descriptor for billing_type field.
- usagelogDescBillingType := usagelogFields[20].Descriptor()
+ usagelogDescBillingType := usagelogFields[21].Descriptor()
// usagelog.DefaultBillingType holds the default value on creation for the billing_type field.
usagelog.DefaultBillingType = usagelogDescBillingType.Default.(int8)
// usagelogDescStream is the schema descriptor for stream field.
- usagelogDescStream := usagelogFields[21].Descriptor()
+ usagelogDescStream := usagelogFields[22].Descriptor()
// usagelog.DefaultStream holds the default value on creation for the stream field.
usagelog.DefaultStream = usagelogDescStream.Default.(bool)
// usagelogDescUserAgent is the schema descriptor for user_agent field.
- usagelogDescUserAgent := usagelogFields[24].Descriptor()
+ usagelogDescUserAgent := usagelogFields[25].Descriptor()
// usagelog.UserAgentValidator is a validator for the "user_agent" field. It is called by the builders before save.
usagelog.UserAgentValidator = usagelogDescUserAgent.Validators[0].(func(string) error)
// usagelogDescIPAddress is the schema descriptor for ip_address field.
- usagelogDescIPAddress := usagelogFields[25].Descriptor()
+ usagelogDescIPAddress := usagelogFields[26].Descriptor()
// usagelog.IPAddressValidator is a validator for the "ip_address" field. It is called by the builders before save.
usagelog.IPAddressValidator = usagelogDescIPAddress.Validators[0].(func(string) error)
// usagelogDescImageCount is the schema descriptor for image_count field.
- usagelogDescImageCount := usagelogFields[26].Descriptor()
+ usagelogDescImageCount := usagelogFields[27].Descriptor()
// usagelog.DefaultImageCount holds the default value on creation for the image_count field.
usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int)
// usagelogDescImageSize is the schema descriptor for image_size field.
- usagelogDescImageSize := usagelogFields[27].Descriptor()
+ usagelogDescImageSize := usagelogFields[28].Descriptor()
// usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save.
usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error)
// usagelogDescCreatedAt is the schema descriptor for created_at field.
- usagelogDescCreatedAt := usagelogFields[28].Descriptor()
+ usagelogDescCreatedAt := usagelogFields[29].Descriptor()
// usagelog.DefaultCreatedAt holds the default value on creation for the created_at field.
usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time)
userMixin := schema.User{}.Mixin()
diff --git a/backend/ent/schema/account.go b/backend/ent/schema/account.go
index ec192a97..dd79ba96 100644
--- a/backend/ent/schema/account.go
+++ b/backend/ent/schema/account.go
@@ -102,6 +102,12 @@ func (Account) Fields() []ent.Field {
field.Int("priority").
Default(50),
+ // rate_multiplier: 账号计费倍率(>=0,允许 0 表示该账号计费为 0)
+ // 仅影响账号维度计费口径,不影响用户/API Key 扣费(分组倍率)
+ field.Float("rate_multiplier").
+ SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}).
+ Default(1.0),
+
// status: 账户状态,如 "active", "error", "disabled"
field.String("status").
MaxLen(20).
diff --git a/backend/ent/schema/usage_log.go b/backend/ent/schema/usage_log.go
index 264a4087..fc7c7165 100644
--- a/backend/ent/schema/usage_log.go
+++ b/backend/ent/schema/usage_log.go
@@ -85,6 +85,12 @@ func (UsageLog) Fields() []ent.Field {
Default(1).
SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}),
+ // account_rate_multiplier: 账号计费倍率快照(NULL 表示按 1.0 处理)
+ field.Float("account_rate_multiplier").
+ Optional().
+ Nillable().
+ SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}),
+
// 其他字段
field.Int8("billing_type").
Default(0),
diff --git a/backend/ent/usagelog.go b/backend/ent/usagelog.go
index cd576466..81c466b4 100644
--- a/backend/ent/usagelog.go
+++ b/backend/ent/usagelog.go
@@ -62,6 +62,8 @@ type UsageLog struct {
ActualCost float64 `json:"actual_cost,omitempty"`
// RateMultiplier holds the value of the "rate_multiplier" field.
RateMultiplier float64 `json:"rate_multiplier,omitempty"`
+ // AccountRateMultiplier holds the value of the "account_rate_multiplier" field.
+ AccountRateMultiplier *float64 `json:"account_rate_multiplier,omitempty"`
// BillingType holds the value of the "billing_type" field.
BillingType int8 `json:"billing_type,omitempty"`
// Stream holds the value of the "stream" field.
@@ -165,7 +167,7 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) {
switch columns[i] {
case usagelog.FieldStream:
values[i] = new(sql.NullBool)
- case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier:
+ case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier, usagelog.FieldAccountRateMultiplier:
values[i] = new(sql.NullFloat64)
case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount:
values[i] = new(sql.NullInt64)
@@ -316,6 +318,13 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error {
} else if value.Valid {
_m.RateMultiplier = value.Float64
}
+ case usagelog.FieldAccountRateMultiplier:
+ if value, ok := values[i].(*sql.NullFloat64); !ok {
+ return fmt.Errorf("unexpected type %T for field account_rate_multiplier", values[i])
+ } else if value.Valid {
+ _m.AccountRateMultiplier = new(float64)
+ *_m.AccountRateMultiplier = value.Float64
+ }
case usagelog.FieldBillingType:
if value, ok := values[i].(*sql.NullInt64); !ok {
return fmt.Errorf("unexpected type %T for field billing_type", values[i])
@@ -500,6 +509,11 @@ func (_m *UsageLog) String() string {
builder.WriteString("rate_multiplier=")
builder.WriteString(fmt.Sprintf("%v", _m.RateMultiplier))
builder.WriteString(", ")
+ if v := _m.AccountRateMultiplier; v != nil {
+ builder.WriteString("account_rate_multiplier=")
+ builder.WriteString(fmt.Sprintf("%v", *v))
+ }
+ builder.WriteString(", ")
builder.WriteString("billing_type=")
builder.WriteString(fmt.Sprintf("%v", _m.BillingType))
builder.WriteString(", ")
diff --git a/backend/ent/usagelog/usagelog.go b/backend/ent/usagelog/usagelog.go
index c06925c4..980f1e58 100644
--- a/backend/ent/usagelog/usagelog.go
+++ b/backend/ent/usagelog/usagelog.go
@@ -54,6 +54,8 @@ const (
FieldActualCost = "actual_cost"
// FieldRateMultiplier holds the string denoting the rate_multiplier field in the database.
FieldRateMultiplier = "rate_multiplier"
+ // FieldAccountRateMultiplier holds the string denoting the account_rate_multiplier field in the database.
+ FieldAccountRateMultiplier = "account_rate_multiplier"
// FieldBillingType holds the string denoting the billing_type field in the database.
FieldBillingType = "billing_type"
// FieldStream holds the string denoting the stream field in the database.
@@ -144,6 +146,7 @@ var Columns = []string{
FieldTotalCost,
FieldActualCost,
FieldRateMultiplier,
+ FieldAccountRateMultiplier,
FieldBillingType,
FieldStream,
FieldDurationMs,
@@ -320,6 +323,11 @@ func ByRateMultiplier(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldRateMultiplier, opts...).ToFunc()
}
+// ByAccountRateMultiplier orders the results by the account_rate_multiplier field.
+func ByAccountRateMultiplier(opts ...sql.OrderTermOption) OrderOption {
+ return sql.OrderByField(FieldAccountRateMultiplier, opts...).ToFunc()
+}
+
// ByBillingType orders the results by the billing_type field.
func ByBillingType(opts ...sql.OrderTermOption) OrderOption {
return sql.OrderByField(FieldBillingType, opts...).ToFunc()
diff --git a/backend/ent/usagelog/where.go b/backend/ent/usagelog/where.go
index 96b7a19c..28e2ab4c 100644
--- a/backend/ent/usagelog/where.go
+++ b/backend/ent/usagelog/where.go
@@ -155,6 +155,11 @@ func RateMultiplier(v float64) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldRateMultiplier, v))
}
+// AccountRateMultiplier applies equality check predicate on the "account_rate_multiplier" field. It's identical to AccountRateMultiplierEQ.
+func AccountRateMultiplier(v float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldEQ(FieldAccountRateMultiplier, v))
+}
+
// BillingType applies equality check predicate on the "billing_type" field. It's identical to BillingTypeEQ.
func BillingType(v int8) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldBillingType, v))
@@ -970,6 +975,56 @@ func RateMultiplierLTE(v float64) predicate.UsageLog {
return predicate.UsageLog(sql.FieldLTE(FieldRateMultiplier, v))
}
+// AccountRateMultiplierEQ applies the EQ predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierEQ(v float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldEQ(FieldAccountRateMultiplier, v))
+}
+
+// AccountRateMultiplierNEQ applies the NEQ predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierNEQ(v float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldNEQ(FieldAccountRateMultiplier, v))
+}
+
+// AccountRateMultiplierIn applies the In predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierIn(vs ...float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldIn(FieldAccountRateMultiplier, vs...))
+}
+
+// AccountRateMultiplierNotIn applies the NotIn predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierNotIn(vs ...float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldNotIn(FieldAccountRateMultiplier, vs...))
+}
+
+// AccountRateMultiplierGT applies the GT predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierGT(v float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldGT(FieldAccountRateMultiplier, v))
+}
+
+// AccountRateMultiplierGTE applies the GTE predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierGTE(v float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldGTE(FieldAccountRateMultiplier, v))
+}
+
+// AccountRateMultiplierLT applies the LT predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierLT(v float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldLT(FieldAccountRateMultiplier, v))
+}
+
+// AccountRateMultiplierLTE applies the LTE predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierLTE(v float64) predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldLTE(FieldAccountRateMultiplier, v))
+}
+
+// AccountRateMultiplierIsNil applies the IsNil predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierIsNil() predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldIsNull(FieldAccountRateMultiplier))
+}
+
+// AccountRateMultiplierNotNil applies the NotNil predicate on the "account_rate_multiplier" field.
+func AccountRateMultiplierNotNil() predicate.UsageLog {
+ return predicate.UsageLog(sql.FieldNotNull(FieldAccountRateMultiplier))
+}
+
// BillingTypeEQ applies the EQ predicate on the "billing_type" field.
func BillingTypeEQ(v int8) predicate.UsageLog {
return predicate.UsageLog(sql.FieldEQ(FieldBillingType, v))
diff --git a/backend/ent/usagelog_create.go b/backend/ent/usagelog_create.go
index e63fab05..a17d6507 100644
--- a/backend/ent/usagelog_create.go
+++ b/backend/ent/usagelog_create.go
@@ -267,6 +267,20 @@ func (_c *UsageLogCreate) SetNillableRateMultiplier(v *float64) *UsageLogCreate
return _c
}
+// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
+func (_c *UsageLogCreate) SetAccountRateMultiplier(v float64) *UsageLogCreate {
+ _c.mutation.SetAccountRateMultiplier(v)
+ return _c
+}
+
+// SetNillableAccountRateMultiplier sets the "account_rate_multiplier" field if the given value is not nil.
+func (_c *UsageLogCreate) SetNillableAccountRateMultiplier(v *float64) *UsageLogCreate {
+ if v != nil {
+ _c.SetAccountRateMultiplier(*v)
+ }
+ return _c
+}
+
// SetBillingType sets the "billing_type" field.
func (_c *UsageLogCreate) SetBillingType(v int8) *UsageLogCreate {
_c.mutation.SetBillingType(v)
@@ -712,6 +726,10 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) {
_spec.SetField(usagelog.FieldRateMultiplier, field.TypeFloat64, value)
_node.RateMultiplier = value
}
+ if value, ok := _c.mutation.AccountRateMultiplier(); ok {
+ _spec.SetField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
+ _node.AccountRateMultiplier = &value
+ }
if value, ok := _c.mutation.BillingType(); ok {
_spec.SetField(usagelog.FieldBillingType, field.TypeInt8, value)
_node.BillingType = value
@@ -1215,6 +1233,30 @@ func (u *UsageLogUpsert) AddRateMultiplier(v float64) *UsageLogUpsert {
return u
}
+// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
+func (u *UsageLogUpsert) SetAccountRateMultiplier(v float64) *UsageLogUpsert {
+ u.Set(usagelog.FieldAccountRateMultiplier, v)
+ return u
+}
+
+// UpdateAccountRateMultiplier sets the "account_rate_multiplier" field to the value that was provided on create.
+func (u *UsageLogUpsert) UpdateAccountRateMultiplier() *UsageLogUpsert {
+ u.SetExcluded(usagelog.FieldAccountRateMultiplier)
+ return u
+}
+
+// AddAccountRateMultiplier adds v to the "account_rate_multiplier" field.
+func (u *UsageLogUpsert) AddAccountRateMultiplier(v float64) *UsageLogUpsert {
+ u.Add(usagelog.FieldAccountRateMultiplier, v)
+ return u
+}
+
+// ClearAccountRateMultiplier clears the value of the "account_rate_multiplier" field.
+func (u *UsageLogUpsert) ClearAccountRateMultiplier() *UsageLogUpsert {
+ u.SetNull(usagelog.FieldAccountRateMultiplier)
+ return u
+}
+
// SetBillingType sets the "billing_type" field.
func (u *UsageLogUpsert) SetBillingType(v int8) *UsageLogUpsert {
u.Set(usagelog.FieldBillingType, v)
@@ -1795,6 +1837,34 @@ func (u *UsageLogUpsertOne) UpdateRateMultiplier() *UsageLogUpsertOne {
})
}
+// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
+func (u *UsageLogUpsertOne) SetAccountRateMultiplier(v float64) *UsageLogUpsertOne {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.SetAccountRateMultiplier(v)
+ })
+}
+
+// AddAccountRateMultiplier adds v to the "account_rate_multiplier" field.
+func (u *UsageLogUpsertOne) AddAccountRateMultiplier(v float64) *UsageLogUpsertOne {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.AddAccountRateMultiplier(v)
+ })
+}
+
+// UpdateAccountRateMultiplier sets the "account_rate_multiplier" field to the value that was provided on create.
+func (u *UsageLogUpsertOne) UpdateAccountRateMultiplier() *UsageLogUpsertOne {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.UpdateAccountRateMultiplier()
+ })
+}
+
+// ClearAccountRateMultiplier clears the value of the "account_rate_multiplier" field.
+func (u *UsageLogUpsertOne) ClearAccountRateMultiplier() *UsageLogUpsertOne {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.ClearAccountRateMultiplier()
+ })
+}
+
// SetBillingType sets the "billing_type" field.
func (u *UsageLogUpsertOne) SetBillingType(v int8) *UsageLogUpsertOne {
return u.Update(func(s *UsageLogUpsert) {
@@ -2566,6 +2636,34 @@ func (u *UsageLogUpsertBulk) UpdateRateMultiplier() *UsageLogUpsertBulk {
})
}
+// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
+func (u *UsageLogUpsertBulk) SetAccountRateMultiplier(v float64) *UsageLogUpsertBulk {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.SetAccountRateMultiplier(v)
+ })
+}
+
+// AddAccountRateMultiplier adds v to the "account_rate_multiplier" field.
+func (u *UsageLogUpsertBulk) AddAccountRateMultiplier(v float64) *UsageLogUpsertBulk {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.AddAccountRateMultiplier(v)
+ })
+}
+
+// UpdateAccountRateMultiplier sets the "account_rate_multiplier" field to the value that was provided on create.
+func (u *UsageLogUpsertBulk) UpdateAccountRateMultiplier() *UsageLogUpsertBulk {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.UpdateAccountRateMultiplier()
+ })
+}
+
+// ClearAccountRateMultiplier clears the value of the "account_rate_multiplier" field.
+func (u *UsageLogUpsertBulk) ClearAccountRateMultiplier() *UsageLogUpsertBulk {
+ return u.Update(func(s *UsageLogUpsert) {
+ s.ClearAccountRateMultiplier()
+ })
+}
+
// SetBillingType sets the "billing_type" field.
func (u *UsageLogUpsertBulk) SetBillingType(v int8) *UsageLogUpsertBulk {
return u.Update(func(s *UsageLogUpsert) {
diff --git a/backend/ent/usagelog_update.go b/backend/ent/usagelog_update.go
index ec2acbbb..571a7b3c 100644
--- a/backend/ent/usagelog_update.go
+++ b/backend/ent/usagelog_update.go
@@ -415,6 +415,33 @@ func (_u *UsageLogUpdate) AddRateMultiplier(v float64) *UsageLogUpdate {
return _u
}
+// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
+func (_u *UsageLogUpdate) SetAccountRateMultiplier(v float64) *UsageLogUpdate {
+ _u.mutation.ResetAccountRateMultiplier()
+ _u.mutation.SetAccountRateMultiplier(v)
+ return _u
+}
+
+// SetNillableAccountRateMultiplier sets the "account_rate_multiplier" field if the given value is not nil.
+func (_u *UsageLogUpdate) SetNillableAccountRateMultiplier(v *float64) *UsageLogUpdate {
+ if v != nil {
+ _u.SetAccountRateMultiplier(*v)
+ }
+ return _u
+}
+
+// AddAccountRateMultiplier adds value to the "account_rate_multiplier" field.
+func (_u *UsageLogUpdate) AddAccountRateMultiplier(v float64) *UsageLogUpdate {
+ _u.mutation.AddAccountRateMultiplier(v)
+ return _u
+}
+
+// ClearAccountRateMultiplier clears the value of the "account_rate_multiplier" field.
+func (_u *UsageLogUpdate) ClearAccountRateMultiplier() *UsageLogUpdate {
+ _u.mutation.ClearAccountRateMultiplier()
+ return _u
+}
+
// SetBillingType sets the "billing_type" field.
func (_u *UsageLogUpdate) SetBillingType(v int8) *UsageLogUpdate {
_u.mutation.ResetBillingType()
@@ -807,6 +834,15 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if value, ok := _u.mutation.AddedRateMultiplier(); ok {
_spec.AddField(usagelog.FieldRateMultiplier, field.TypeFloat64, value)
}
+ if value, ok := _u.mutation.AccountRateMultiplier(); ok {
+ _spec.SetField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
+ }
+ if value, ok := _u.mutation.AddedAccountRateMultiplier(); ok {
+ _spec.AddField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
+ }
+ if _u.mutation.AccountRateMultiplierCleared() {
+ _spec.ClearField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64)
+ }
if value, ok := _u.mutation.BillingType(); ok {
_spec.SetField(usagelog.FieldBillingType, field.TypeInt8, value)
}
@@ -1406,6 +1442,33 @@ func (_u *UsageLogUpdateOne) AddRateMultiplier(v float64) *UsageLogUpdateOne {
return _u
}
+// SetAccountRateMultiplier sets the "account_rate_multiplier" field.
+func (_u *UsageLogUpdateOne) SetAccountRateMultiplier(v float64) *UsageLogUpdateOne {
+ _u.mutation.ResetAccountRateMultiplier()
+ _u.mutation.SetAccountRateMultiplier(v)
+ return _u
+}
+
+// SetNillableAccountRateMultiplier sets the "account_rate_multiplier" field if the given value is not nil.
+func (_u *UsageLogUpdateOne) SetNillableAccountRateMultiplier(v *float64) *UsageLogUpdateOne {
+ if v != nil {
+ _u.SetAccountRateMultiplier(*v)
+ }
+ return _u
+}
+
+// AddAccountRateMultiplier adds value to the "account_rate_multiplier" field.
+func (_u *UsageLogUpdateOne) AddAccountRateMultiplier(v float64) *UsageLogUpdateOne {
+ _u.mutation.AddAccountRateMultiplier(v)
+ return _u
+}
+
+// ClearAccountRateMultiplier clears the value of the "account_rate_multiplier" field.
+func (_u *UsageLogUpdateOne) ClearAccountRateMultiplier() *UsageLogUpdateOne {
+ _u.mutation.ClearAccountRateMultiplier()
+ return _u
+}
+
// SetBillingType sets the "billing_type" field.
func (_u *UsageLogUpdateOne) SetBillingType(v int8) *UsageLogUpdateOne {
_u.mutation.ResetBillingType()
@@ -1828,6 +1891,15 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err
if value, ok := _u.mutation.AddedRateMultiplier(); ok {
_spec.AddField(usagelog.FieldRateMultiplier, field.TypeFloat64, value)
}
+ if value, ok := _u.mutation.AccountRateMultiplier(); ok {
+ _spec.SetField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
+ }
+ if value, ok := _u.mutation.AddedAccountRateMultiplier(); ok {
+ _spec.AddField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64, value)
+ }
+ if _u.mutation.AccountRateMultiplierCleared() {
+ _spec.ClearField(usagelog.FieldAccountRateMultiplier, field.TypeFloat64)
+ }
if value, ok := _u.mutation.BillingType(); ok {
_spec.SetField(usagelog.FieldBillingType, field.TypeInt8, value)
}
diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go
index 8a7270e5..92fdf2eb 100644
--- a/backend/internal/handler/admin/account_handler.go
+++ b/backend/internal/handler/admin/account_handler.go
@@ -84,6 +84,7 @@ type CreateAccountRequest struct {
ProxyID *int64 `json:"proxy_id"`
Concurrency int `json:"concurrency"`
Priority int `json:"priority"`
+ RateMultiplier *float64 `json:"rate_multiplier"`
GroupIDs []int64 `json:"group_ids"`
ExpiresAt *int64 `json:"expires_at"`
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
@@ -101,6 +102,7 @@ type UpdateAccountRequest struct {
ProxyID *int64 `json:"proxy_id"`
Concurrency *int `json:"concurrency"`
Priority *int `json:"priority"`
+ RateMultiplier *float64 `json:"rate_multiplier"`
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
GroupIDs *[]int64 `json:"group_ids"`
ExpiresAt *int64 `json:"expires_at"`
@@ -115,6 +117,7 @@ type BulkUpdateAccountsRequest struct {
ProxyID *int64 `json:"proxy_id"`
Concurrency *int `json:"concurrency"`
Priority *int `json:"priority"`
+ RateMultiplier *float64 `json:"rate_multiplier"`
Status string `json:"status" binding:"omitempty,oneof=active inactive error"`
Schedulable *bool `json:"schedulable"`
GroupIDs *[]int64 `json:"group_ids"`
@@ -199,6 +202,10 @@ func (h *AccountHandler) Create(c *gin.Context) {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
+ if req.RateMultiplier != nil && *req.RateMultiplier < 0 {
+ response.BadRequest(c, "rate_multiplier must be >= 0")
+ return
+ }
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
@@ -213,6 +220,7 @@ func (h *AccountHandler) Create(c *gin.Context) {
ProxyID: req.ProxyID,
Concurrency: req.Concurrency,
Priority: req.Priority,
+ RateMultiplier: req.RateMultiplier,
GroupIDs: req.GroupIDs,
ExpiresAt: req.ExpiresAt,
AutoPauseOnExpired: req.AutoPauseOnExpired,
@@ -258,6 +266,10 @@ func (h *AccountHandler) Update(c *gin.Context) {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
+ if req.RateMultiplier != nil && *req.RateMultiplier < 0 {
+ response.BadRequest(c, "rate_multiplier must be >= 0")
+ return
+ }
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
@@ -271,6 +283,7 @@ func (h *AccountHandler) Update(c *gin.Context) {
ProxyID: req.ProxyID,
Concurrency: req.Concurrency, // 指针类型,nil 表示未提供
Priority: req.Priority, // 指针类型,nil 表示未提供
+ RateMultiplier: req.RateMultiplier,
Status: req.Status,
GroupIDs: req.GroupIDs,
ExpiresAt: req.ExpiresAt,
@@ -652,6 +665,10 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
response.BadRequest(c, "Invalid request: "+err.Error())
return
}
+ if req.RateMultiplier != nil && *req.RateMultiplier < 0 {
+ response.BadRequest(c, "rate_multiplier must be >= 0")
+ return
+ }
// 确定是否跳过混合渠道检查
skipCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
@@ -660,6 +677,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
req.ProxyID != nil ||
req.Concurrency != nil ||
req.Priority != nil ||
+ req.RateMultiplier != nil ||
req.Status != "" ||
req.Schedulable != nil ||
req.GroupIDs != nil ||
@@ -677,6 +695,7 @@ func (h *AccountHandler) BulkUpdate(c *gin.Context) {
ProxyID: req.ProxyID,
Concurrency: req.Concurrency,
Priority: req.Priority,
+ RateMultiplier: req.RateMultiplier,
Status: req.Status,
Schedulable: req.Schedulable,
GroupIDs: req.GroupIDs,
diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go
index 6ffaedea..075d3300 100644
--- a/backend/internal/handler/dto/mappers.go
+++ b/backend/internal/handler/dto/mappers.go
@@ -125,6 +125,7 @@ func AccountFromServiceShallow(a *service.Account) *Account {
ProxyID: a.ProxyID,
Concurrency: a.Concurrency,
Priority: a.Priority,
+ RateMultiplier: a.BillingRateMultiplier(),
Status: a.Status,
ErrorMessage: a.ErrorMessage,
LastUsedAt: a.LastUsedAt,
@@ -279,6 +280,7 @@ func usageLogFromServiceBase(l *service.UsageLog, account *AccountSummary, inclu
TotalCost: l.TotalCost,
ActualCost: l.ActualCost,
RateMultiplier: l.RateMultiplier,
+ AccountRateMultiplier: l.AccountRateMultiplier,
BillingType: l.BillingType,
Stream: l.Stream,
DurationMs: l.DurationMs,
diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go
index a9b010b9..7d7b798f 100644
--- a/backend/internal/handler/dto/types.go
+++ b/backend/internal/handler/dto/types.go
@@ -76,6 +76,7 @@ type Account struct {
ProxyID *int64 `json:"proxy_id"`
Concurrency int `json:"concurrency"`
Priority int `json:"priority"`
+ RateMultiplier float64 `json:"rate_multiplier"`
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
LastUsedAt *time.Time `json:"last_used_at"`
@@ -169,13 +170,14 @@ type UsageLog struct {
CacheCreation5mTokens int `json:"cache_creation_5m_tokens"`
CacheCreation1hTokens int `json:"cache_creation_1h_tokens"`
- InputCost float64 `json:"input_cost"`
- OutputCost float64 `json:"output_cost"`
- CacheCreationCost float64 `json:"cache_creation_cost"`
- CacheReadCost float64 `json:"cache_read_cost"`
- TotalCost float64 `json:"total_cost"`
- ActualCost float64 `json:"actual_cost"`
- RateMultiplier float64 `json:"rate_multiplier"`
+ InputCost float64 `json:"input_cost"`
+ OutputCost float64 `json:"output_cost"`
+ CacheCreationCost float64 `json:"cache_creation_cost"`
+ CacheReadCost float64 `json:"cache_read_cost"`
+ TotalCost float64 `json:"total_cost"`
+ ActualCost float64 `json:"actual_cost"`
+ RateMultiplier float64 `json:"rate_multiplier"`
+ AccountRateMultiplier *float64 `json:"account_rate_multiplier"`
BillingType int8 `json:"billing_type"`
Stream bool `json:"stream"`
diff --git a/backend/internal/pkg/usagestats/account_stats.go b/backend/internal/pkg/usagestats/account_stats.go
index ed77dd27..9ac49625 100644
--- a/backend/internal/pkg/usagestats/account_stats.go
+++ b/backend/internal/pkg/usagestats/account_stats.go
@@ -1,8 +1,14 @@
package usagestats
// AccountStats 账号使用统计
+//
+// cost: 账号口径费用(使用 total_cost * account_rate_multiplier)
+// standard_cost: 标准费用(使用 total_cost,不含倍率)
+// user_cost: 用户/API Key 口径费用(使用 actual_cost,受分组倍率影响)
type AccountStats struct {
- Requests int64 `json:"requests"`
- Tokens int64 `json:"tokens"`
- Cost float64 `json:"cost"`
+ Requests int64 `json:"requests"`
+ Tokens int64 `json:"tokens"`
+ Cost float64 `json:"cost"`
+ StandardCost float64 `json:"standard_cost"`
+ UserCost float64 `json:"user_cost"`
}
diff --git a/backend/internal/pkg/usagestats/usage_log_types.go b/backend/internal/pkg/usagestats/usage_log_types.go
index 3952785b..2f6c7fe0 100644
--- a/backend/internal/pkg/usagestats/usage_log_types.go
+++ b/backend/internal/pkg/usagestats/usage_log_types.go
@@ -147,14 +147,15 @@ type UsageLogFilters struct {
// UsageStats represents usage statistics
type UsageStats struct {
- TotalRequests int64 `json:"total_requests"`
- TotalInputTokens int64 `json:"total_input_tokens"`
- TotalOutputTokens int64 `json:"total_output_tokens"`
- TotalCacheTokens int64 `json:"total_cache_tokens"`
- TotalTokens int64 `json:"total_tokens"`
- TotalCost float64 `json:"total_cost"`
- TotalActualCost float64 `json:"total_actual_cost"`
- AverageDurationMs float64 `json:"average_duration_ms"`
+ TotalRequests int64 `json:"total_requests"`
+ TotalInputTokens int64 `json:"total_input_tokens"`
+ TotalOutputTokens int64 `json:"total_output_tokens"`
+ TotalCacheTokens int64 `json:"total_cache_tokens"`
+ TotalTokens int64 `json:"total_tokens"`
+ TotalCost float64 `json:"total_cost"`
+ TotalActualCost float64 `json:"total_actual_cost"`
+ TotalAccountCost *float64 `json:"total_account_cost,omitempty"`
+ AverageDurationMs float64 `json:"average_duration_ms"`
}
// BatchUserUsageStats represents usage stats for a single user
@@ -177,25 +178,29 @@ type AccountUsageHistory struct {
Label string `json:"label"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
- Cost float64 `json:"cost"`
- ActualCost float64 `json:"actual_cost"`
+ Cost float64 `json:"cost"` // 标准计费(total_cost)
+ ActualCost float64 `json:"actual_cost"` // 账号口径费用(total_cost * account_rate_multiplier)
+ UserCost float64 `json:"user_cost"` // 用户口径费用(actual_cost,受分组倍率影响)
}
// AccountUsageSummary represents summary statistics for an account
type AccountUsageSummary struct {
Days int `json:"days"`
ActualDaysUsed int `json:"actual_days_used"`
- TotalCost float64 `json:"total_cost"`
+ TotalCost float64 `json:"total_cost"` // 账号口径费用
+ TotalUserCost float64 `json:"total_user_cost"` // 用户口径费用
TotalStandardCost float64 `json:"total_standard_cost"`
TotalRequests int64 `json:"total_requests"`
TotalTokens int64 `json:"total_tokens"`
- AvgDailyCost float64 `json:"avg_daily_cost"`
+ AvgDailyCost float64 `json:"avg_daily_cost"` // 账号口径日均
+ AvgDailyUserCost float64 `json:"avg_daily_user_cost"`
AvgDailyRequests float64 `json:"avg_daily_requests"`
AvgDailyTokens float64 `json:"avg_daily_tokens"`
AvgDurationMs float64 `json:"avg_duration_ms"`
Today *struct {
Date string `json:"date"`
Cost float64 `json:"cost"`
+ UserCost float64 `json:"user_cost"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
} `json:"today"`
@@ -203,6 +208,7 @@ type AccountUsageSummary struct {
Date string `json:"date"`
Label string `json:"label"`
Cost float64 `json:"cost"`
+ UserCost float64 `json:"user_cost"`
Requests int64 `json:"requests"`
} `json:"highest_cost_day"`
HighestRequestDay *struct {
@@ -210,6 +216,7 @@ type AccountUsageSummary struct {
Label string `json:"label"`
Requests int64 `json:"requests"`
Cost float64 `json:"cost"`
+ UserCost float64 `json:"user_cost"`
} `json:"highest_request_day"`
}
diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go
index aaa89f21..ba8751df 100644
--- a/backend/internal/repository/account_repo.go
+++ b/backend/internal/repository/account_repo.go
@@ -80,6 +80,10 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
SetSchedulable(account.Schedulable).
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
+ if account.RateMultiplier != nil {
+ builder.SetRateMultiplier(*account.RateMultiplier)
+ }
+
if account.ProxyID != nil {
builder.SetProxyID(*account.ProxyID)
}
@@ -291,6 +295,10 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
SetSchedulable(account.Schedulable).
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
+ if account.RateMultiplier != nil {
+ builder.SetRateMultiplier(*account.RateMultiplier)
+ }
+
if account.ProxyID != nil {
builder.SetProxyID(*account.ProxyID)
} else {
@@ -999,6 +1007,11 @@ func (r *accountRepository) BulkUpdate(ctx context.Context, ids []int64, updates
args = append(args, *updates.Priority)
idx++
}
+ if updates.RateMultiplier != nil {
+ setClauses = append(setClauses, "rate_multiplier = $"+itoa(idx))
+ args = append(args, *updates.RateMultiplier)
+ idx++
+ }
if updates.Status != nil {
setClauses = append(setClauses, "status = $"+itoa(idx))
args = append(args, *updates.Status)
@@ -1347,6 +1360,8 @@ func accountEntityToService(m *dbent.Account) *service.Account {
return nil
}
+ rateMultiplier := m.RateMultiplier
+
return &service.Account{
ID: m.ID,
Name: m.Name,
@@ -1358,6 +1373,7 @@ func accountEntityToService(m *dbent.Account) *service.Account {
ProxyID: m.ProxyID,
Concurrency: m.Concurrency,
Priority: m.Priority,
+ RateMultiplier: &rateMultiplier,
Status: m.Status,
ErrorMessage: derefString(m.ErrorMessage),
LastUsedAt: m.LastUsedAt,
diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go
index e483f89f..43969b59 100644
--- a/backend/internal/repository/usage_log_repo.go
+++ b/backend/internal/repository/usage_log_repo.go
@@ -22,7 +22,7 @@ import (
"github.com/lib/pq"
)
-const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, created_at"
+const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, account_rate_multiplier, billing_type, stream, duration_ms, first_token_ms, user_agent, ip_address, image_count, image_size, created_at"
type usageLogRepository struct {
client *dbent.Client
@@ -105,6 +105,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
total_cost,
actual_cost,
rate_multiplier,
+ account_rate_multiplier,
billing_type,
stream,
duration_ms,
@@ -120,7 +121,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
$8, $9, $10, $11,
$12, $13,
$14, $15, $16, $17, $18, $19,
- $20, $21, $22, $23, $24, $25, $26, $27, $28, $29
+ $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30
)
ON CONFLICT (request_id, api_key_id) DO NOTHING
RETURNING id, created_at
@@ -160,6 +161,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog)
log.TotalCost,
log.ActualCost,
rateMultiplier,
+ log.AccountRateMultiplier,
log.BillingType,
log.Stream,
duration,
@@ -835,7 +837,9 @@ func (r *usageLogRepository) GetAccountTodayStats(ctx context.Context, accountID
SELECT
COUNT(*) as requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
- COALESCE(SUM(actual_cost), 0) as cost
+ COALESCE(SUM(total_cost * COALESCE(account_rate_multiplier, 1)), 0) as cost,
+ COALESCE(SUM(total_cost), 0) as standard_cost,
+ COALESCE(SUM(actual_cost), 0) as user_cost
FROM usage_logs
WHERE account_id = $1 AND created_at >= $2
`
@@ -849,6 +853,8 @@ func (r *usageLogRepository) GetAccountTodayStats(ctx context.Context, accountID
&stats.Requests,
&stats.Tokens,
&stats.Cost,
+ &stats.StandardCost,
+ &stats.UserCost,
); err != nil {
return nil, err
}
@@ -861,7 +867,9 @@ func (r *usageLogRepository) GetAccountWindowStats(ctx context.Context, accountI
SELECT
COUNT(*) as requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
- COALESCE(SUM(actual_cost), 0) as cost
+ COALESCE(SUM(total_cost * COALESCE(account_rate_multiplier, 1)), 0) as cost,
+ COALESCE(SUM(total_cost), 0) as standard_cost,
+ COALESCE(SUM(actual_cost), 0) as user_cost
FROM usage_logs
WHERE account_id = $1 AND created_at >= $2
`
@@ -875,6 +883,8 @@ func (r *usageLogRepository) GetAccountWindowStats(ctx context.Context, accountI
&stats.Requests,
&stats.Tokens,
&stats.Cost,
+ &stats.StandardCost,
+ &stats.UserCost,
); err != nil {
return nil, err
}
@@ -1454,7 +1464,13 @@ func (r *usageLogRepository) GetUsageTrendWithFilters(ctx context.Context, start
// GetModelStatsWithFilters returns model statistics with optional user/api_key filters
func (r *usageLogRepository) GetModelStatsWithFilters(ctx context.Context, startTime, endTime time.Time, userID, apiKeyID, accountID int64) (results []ModelStat, err error) {
- query := `
+ 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(total_cost * COALESCE(account_rate_multiplier, 1)), 0) as actual_cost"
+ }
+
+ query := fmt.Sprintf(`
SELECT
model,
COUNT(*) as requests,
@@ -1462,10 +1478,10 @@ func (r *usageLogRepository) GetModelStatsWithFilters(ctx context.Context, start
COALESCE(SUM(output_tokens), 0) as output_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
+ %s
FROM usage_logs
WHERE created_at >= $1 AND created_at < $2
- `
+ `, actualCostExpr)
args := []any{startTime, endTime}
if userID > 0 {
@@ -1587,12 +1603,14 @@ func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters Us
COALESCE(SUM(cache_creation_tokens + cache_read_tokens), 0) as total_cache_tokens,
COALESCE(SUM(total_cost), 0) as total_cost,
COALESCE(SUM(actual_cost), 0) as total_actual_cost,
+ COALESCE(SUM(total_cost * COALESCE(account_rate_multiplier, 1)), 0) as total_account_cost,
COALESCE(AVG(duration_ms), 0) as avg_duration_ms
FROM usage_logs
%s
`, buildWhere(conditions))
stats := &UsageStats{}
+ var totalAccountCost float64
if err := scanSingleRow(
ctx,
r.sql,
@@ -1604,10 +1622,14 @@ func (r *usageLogRepository) GetStatsWithFilters(ctx context.Context, filters Us
&stats.TotalCacheTokens,
&stats.TotalCost,
&stats.TotalActualCost,
+ &totalAccountCost,
&stats.AverageDurationMs,
); err != nil {
return nil, err
}
+ if filters.AccountID > 0 {
+ stats.TotalAccountCost = &totalAccountCost
+ }
stats.TotalTokens = stats.TotalInputTokens + stats.TotalOutputTokens + stats.TotalCacheTokens
return stats, nil
}
@@ -1634,7 +1656,8 @@ func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID
COUNT(*) as requests,
COALESCE(SUM(input_tokens + output_tokens + cache_creation_tokens + cache_read_tokens), 0) as tokens,
COALESCE(SUM(total_cost), 0) as cost,
- COALESCE(SUM(actual_cost), 0) as actual_cost
+ COALESCE(SUM(total_cost * COALESCE(account_rate_multiplier, 1)), 0) as actual_cost,
+ COALESCE(SUM(actual_cost), 0) as user_cost
FROM usage_logs
WHERE account_id = $1 AND created_at >= $2 AND created_at < $3
GROUP BY date
@@ -1661,7 +1684,8 @@ func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID
var tokens int64
var cost float64
var actualCost float64
- if err = rows.Scan(&date, &requests, &tokens, &cost, &actualCost); err != nil {
+ var userCost float64
+ if err = rows.Scan(&date, &requests, &tokens, &cost, &actualCost, &userCost); err != nil {
return nil, err
}
t, _ := time.Parse("2006-01-02", date)
@@ -1672,19 +1696,21 @@ func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID
Tokens: tokens,
Cost: cost,
ActualCost: actualCost,
+ UserCost: userCost,
})
}
if err = rows.Err(); err != nil {
return nil, err
}
- var totalActualCost, totalStandardCost float64
+ var totalAccountCost, totalUserCost, totalStandardCost float64
var totalRequests, totalTokens int64
var highestCostDay, highestRequestDay *AccountUsageHistory
for i := range history {
h := &history[i]
- totalActualCost += h.ActualCost
+ totalAccountCost += h.ActualCost
+ totalUserCost += h.UserCost
totalStandardCost += h.Cost
totalRequests += h.Requests
totalTokens += h.Tokens
@@ -1711,11 +1737,13 @@ func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID
summary := AccountUsageSummary{
Days: daysCount,
ActualDaysUsed: actualDaysUsed,
- TotalCost: totalActualCost,
+ TotalCost: totalAccountCost,
+ TotalUserCost: totalUserCost,
TotalStandardCost: totalStandardCost,
TotalRequests: totalRequests,
TotalTokens: totalTokens,
- AvgDailyCost: totalActualCost / float64(actualDaysUsed),
+ AvgDailyCost: totalAccountCost / float64(actualDaysUsed),
+ AvgDailyUserCost: totalUserCost / float64(actualDaysUsed),
AvgDailyRequests: float64(totalRequests) / float64(actualDaysUsed),
AvgDailyTokens: float64(totalTokens) / float64(actualDaysUsed),
AvgDurationMs: avgDuration,
@@ -1727,11 +1755,13 @@ func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID
summary.Today = &struct {
Date string `json:"date"`
Cost float64 `json:"cost"`
+ UserCost float64 `json:"user_cost"`
Requests int64 `json:"requests"`
Tokens int64 `json:"tokens"`
}{
Date: history[i].Date,
Cost: history[i].ActualCost,
+ UserCost: history[i].UserCost,
Requests: history[i].Requests,
Tokens: history[i].Tokens,
}
@@ -1744,11 +1774,13 @@ func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID
Date string `json:"date"`
Label string `json:"label"`
Cost float64 `json:"cost"`
+ UserCost float64 `json:"user_cost"`
Requests int64 `json:"requests"`
}{
Date: highestCostDay.Date,
Label: highestCostDay.Label,
Cost: highestCostDay.ActualCost,
+ UserCost: highestCostDay.UserCost,
Requests: highestCostDay.Requests,
}
}
@@ -1759,11 +1791,13 @@ func (r *usageLogRepository) GetAccountUsageStats(ctx context.Context, accountID
Label string `json:"label"`
Requests int64 `json:"requests"`
Cost float64 `json:"cost"`
+ UserCost float64 `json:"user_cost"`
}{
Date: highestRequestDay.Date,
Label: highestRequestDay.Label,
Requests: highestRequestDay.Requests,
Cost: highestRequestDay.ActualCost,
+ UserCost: highestRequestDay.UserCost,
}
}
@@ -1994,36 +2028,37 @@ func (r *usageLogRepository) loadSubscriptions(ctx context.Context, ids []int64)
func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, error) {
var (
- id int64
- userID int64
- apiKeyID int64
- accountID int64
- requestID sql.NullString
- model string
- groupID sql.NullInt64
- subscriptionID sql.NullInt64
- inputTokens int
- outputTokens int
- cacheCreationTokens int
- cacheReadTokens int
- cacheCreation5m int
- cacheCreation1h int
- inputCost float64
- outputCost float64
- cacheCreationCost float64
- cacheReadCost float64
- totalCost float64
- actualCost float64
- rateMultiplier float64
- billingType int16
- stream bool
- durationMs sql.NullInt64
- firstTokenMs sql.NullInt64
- userAgent sql.NullString
- ipAddress sql.NullString
- imageCount int
- imageSize sql.NullString
- createdAt time.Time
+ id int64
+ userID int64
+ apiKeyID int64
+ accountID int64
+ requestID sql.NullString
+ model string
+ groupID sql.NullInt64
+ subscriptionID sql.NullInt64
+ inputTokens int
+ outputTokens int
+ cacheCreationTokens int
+ cacheReadTokens int
+ cacheCreation5m int
+ cacheCreation1h int
+ inputCost float64
+ outputCost float64
+ cacheCreationCost float64
+ cacheReadCost float64
+ totalCost float64
+ actualCost float64
+ rateMultiplier float64
+ accountRateMultiplier sql.NullFloat64
+ billingType int16
+ stream bool
+ durationMs sql.NullInt64
+ firstTokenMs sql.NullInt64
+ userAgent sql.NullString
+ ipAddress sql.NullString
+ imageCount int
+ imageSize sql.NullString
+ createdAt time.Time
)
if err := scanner.Scan(
@@ -2048,6 +2083,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
&totalCost,
&actualCost,
&rateMultiplier,
+ &accountRateMultiplier,
&billingType,
&stream,
&durationMs,
@@ -2080,6 +2116,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e
TotalCost: totalCost,
ActualCost: actualCost,
RateMultiplier: rateMultiplier,
+ AccountRateMultiplier: nullFloat64Ptr(accountRateMultiplier),
BillingType: int8(billingType),
Stream: stream,
ImageCount: imageCount,
@@ -2186,6 +2223,14 @@ func nullInt(v *int) 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{}
diff --git a/backend/internal/repository/usage_log_repo_integration_test.go b/backend/internal/repository/usage_log_repo_integration_test.go
index 3f90e49e..126209e2 100644
--- a/backend/internal/repository/usage_log_repo_integration_test.go
+++ b/backend/internal/repository/usage_log_repo_integration_test.go
@@ -11,6 +11,7 @@ import (
dbent "github.com/Wei-Shaw/sub2api/ent"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/internal/pkg/usagestats"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/stretchr/testify/suite"
@@ -95,6 +96,34 @@ func (s *UsageLogRepoSuite) TestGetByID_NotFound() {
s.Require().Error(err, "expected error for non-existent ID")
}
+func (s *UsageLogRepoSuite) TestGetByID_ReturnsAccountRateMultiplier() {
+ user := mustCreateUser(s.T(), s.client, &service.User{Email: "getbyid-mult@test.com"})
+ apiKey := mustCreateApiKey(s.T(), s.client, &service.APIKey{UserID: user.ID, Key: "sk-getbyid-mult", Name: "k"})
+ account := mustCreateAccount(s.T(), s.client, &service.Account{Name: "acc-getbyid-mult"})
+
+ m := 0.5
+ log := &service.UsageLog{
+ UserID: user.ID,
+ APIKeyID: apiKey.ID,
+ AccountID: account.ID,
+ RequestID: uuid.New().String(),
+ Model: "claude-3",
+ InputTokens: 10,
+ OutputTokens: 20,
+ TotalCost: 1.0,
+ ActualCost: 2.0,
+ AccountRateMultiplier: &m,
+ CreatedAt: timezone.Today().Add(2 * time.Hour),
+ }
+ _, err := s.repo.Create(s.ctx, log)
+ s.Require().NoError(err)
+
+ got, err := s.repo.GetByID(s.ctx, log.ID)
+ s.Require().NoError(err)
+ s.Require().NotNil(got.AccountRateMultiplier)
+ s.Require().InEpsilon(0.5, *got.AccountRateMultiplier, 0.0001)
+}
+
// --- Delete ---
func (s *UsageLogRepoSuite) TestDelete() {
@@ -403,12 +432,49 @@ func (s *UsageLogRepoSuite) TestGetAccountTodayStats() {
apiKey := mustCreateApiKey(s.T(), s.client, &service.APIKey{UserID: user.ID, Key: "sk-acctoday", Name: "k"})
account := mustCreateAccount(s.T(), s.client, &service.Account{Name: "acc-today"})
- s.createUsageLog(user, apiKey, account, 10, 20, 0.5, time.Now())
+ createdAt := timezone.Today().Add(1 * time.Hour)
+
+ m1 := 1.5
+ m2 := 0.0
+ _, err := s.repo.Create(s.ctx, &service.UsageLog{
+ UserID: user.ID,
+ APIKeyID: apiKey.ID,
+ AccountID: account.ID,
+ RequestID: uuid.New().String(),
+ Model: "claude-3",
+ InputTokens: 10,
+ OutputTokens: 20,
+ TotalCost: 1.0,
+ ActualCost: 2.0,
+ AccountRateMultiplier: &m1,
+ CreatedAt: createdAt,
+ })
+ s.Require().NoError(err)
+ _, err = s.repo.Create(s.ctx, &service.UsageLog{
+ UserID: user.ID,
+ APIKeyID: apiKey.ID,
+ AccountID: account.ID,
+ RequestID: uuid.New().String(),
+ Model: "claude-3",
+ InputTokens: 5,
+ OutputTokens: 5,
+ TotalCost: 0.5,
+ ActualCost: 1.0,
+ AccountRateMultiplier: &m2,
+ CreatedAt: createdAt,
+ })
+ s.Require().NoError(err)
stats, err := s.repo.GetAccountTodayStats(s.ctx, account.ID)
s.Require().NoError(err, "GetAccountTodayStats")
- s.Require().Equal(int64(1), stats.Requests)
- s.Require().Equal(int64(30), stats.Tokens)
+ s.Require().Equal(int64(2), stats.Requests)
+ s.Require().Equal(int64(40), stats.Tokens)
+ // account cost = SUM(total_cost * account_rate_multiplier)
+ s.Require().InEpsilon(1.5, stats.Cost, 0.0001)
+ // standard cost = SUM(total_cost)
+ s.Require().InEpsilon(1.5, stats.StandardCost, 0.0001)
+ // user cost = SUM(actual_cost)
+ s.Require().InEpsilon(3.0, stats.UserCost, 0.0001)
}
func (s *UsageLogRepoSuite) TestDashboardAggregationConsistency() {
@@ -416,8 +482,8 @@ func (s *UsageLogRepoSuite) TestDashboardAggregationConsistency() {
// 使用固定的时间偏移确保 hour1 和 hour2 在同一天且都在过去
// 选择当天 02:00 和 03:00 作为测试时间点(基于 now 的日期)
dayStart := truncateToDayUTC(now)
- hour1 := dayStart.Add(2 * time.Hour) // 当天 02:00
- hour2 := dayStart.Add(3 * time.Hour) // 当天 03:00
+ hour1 := dayStart.Add(2 * time.Hour) // 当天 02:00
+ hour2 := dayStart.Add(3 * time.Hour) // 当天 03:00
// 如果当前时间早于 hour2,则使用昨天的时间
if now.Before(hour2.Add(time.Hour)) {
dayStart = dayStart.Add(-24 * time.Hour)
diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go
index d96732bd..cc29fec4 100644
--- a/backend/internal/server/api_contract_test.go
+++ b/backend/internal/server/api_contract_test.go
@@ -239,9 +239,10 @@ func TestAPIContracts(t *testing.T) {
"cache_creation_cost": 0,
"cache_read_cost": 0,
"total_cost": 0.5,
- "actual_cost": 0.5,
- "rate_multiplier": 1,
- "billing_type": 0,
+ "actual_cost": 0.5,
+ "rate_multiplier": 1,
+ "account_rate_multiplier": null,
+ "billing_type": 0,
"stream": true,
"duration_ms": 100,
"first_token_ms": 50,
@@ -262,11 +263,11 @@ func TestAPIContracts(t *testing.T) {
name: "GET /api/v1/admin/settings",
setup: func(t *testing.T, deps *contractDeps) {
t.Helper()
- deps.settingRepo.SetAll(map[string]string{
- service.SettingKeyRegistrationEnabled: "true",
- service.SettingKeyEmailVerifyEnabled: "false",
+ deps.settingRepo.SetAll(map[string]string{
+ service.SettingKeyRegistrationEnabled: "true",
+ service.SettingKeyEmailVerifyEnabled: "false",
- service.SettingKeySMTPHost: "smtp.example.com",
+ service.SettingKeySMTPHost: "smtp.example.com",
service.SettingKeySMTPPort: "587",
service.SettingKeySMTPUsername: "user",
service.SettingKeySMTPPassword: "secret",
@@ -285,15 +286,15 @@ func TestAPIContracts(t *testing.T) {
service.SettingKeyContactInfo: "support",
service.SettingKeyDocURL: "https://docs.example.com",
- service.SettingKeyDefaultConcurrency: "5",
- service.SettingKeyDefaultBalance: "1.25",
+ service.SettingKeyDefaultConcurrency: "5",
+ service.SettingKeyDefaultBalance: "1.25",
- service.SettingKeyOpsMonitoringEnabled: "false",
- service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
- service.SettingKeyOpsQueryModeDefault: "auto",
- service.SettingKeyOpsMetricsIntervalSeconds: "60",
- })
- },
+ service.SettingKeyOpsMonitoringEnabled: "false",
+ service.SettingKeyOpsRealtimeMonitoringEnabled: "true",
+ service.SettingKeyOpsQueryModeDefault: "auto",
+ service.SettingKeyOpsMetricsIntervalSeconds: "60",
+ })
+ },
method: http.MethodGet,
path: "/api/v1/admin/settings",
wantStatus: http.StatusOK,
diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go
index cfce9bfa..0d7a9cf9 100644
--- a/backend/internal/service/account.go
+++ b/backend/internal/service/account.go
@@ -9,16 +9,19 @@ import (
)
type Account struct {
- ID int64
- Name string
- Notes *string
- Platform string
- Type string
- Credentials map[string]any
- Extra map[string]any
- ProxyID *int64
- Concurrency int
- Priority int
+ ID int64
+ Name string
+ Notes *string
+ Platform string
+ Type string
+ Credentials map[string]any
+ Extra map[string]any
+ ProxyID *int64
+ Concurrency int
+ Priority int
+ // RateMultiplier 账号计费倍率(>=0,允许 0 表示该账号计费为 0)。
+ // 使用指针用于兼容旧版本调度缓存(Redis)中缺字段的情况:nil 表示按 1.0 处理。
+ RateMultiplier *float64
Status string
ErrorMessage string
LastUsedAt *time.Time
@@ -57,6 +60,20 @@ func (a *Account) IsActive() bool {
return a.Status == StatusActive
}
+// BillingRateMultiplier 返回账号计费倍率。
+// - nil 表示未配置/旧缓存缺字段,按 1.0 处理
+// - 允许 0,表示该账号计费为 0
+// - 负数属于非法数据,出于安全考虑按 1.0 处理
+func (a *Account) BillingRateMultiplier() float64 {
+ if a == nil || a.RateMultiplier == nil {
+ return 1.0
+ }
+ if *a.RateMultiplier < 0 {
+ return 1.0
+ }
+ return *a.RateMultiplier
+}
+
func (a *Account) IsSchedulable() bool {
if !a.IsActive() || !a.Schedulable {
return false
diff --git a/backend/internal/service/account_billing_rate_multiplier_test.go b/backend/internal/service/account_billing_rate_multiplier_test.go
new file mode 100644
index 00000000..731cfa7a
--- /dev/null
+++ b/backend/internal/service/account_billing_rate_multiplier_test.go
@@ -0,0 +1,27 @@
+package service
+
+import (
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestAccount_BillingRateMultiplier_DefaultsToOneWhenNil(t *testing.T) {
+ var a Account
+ require.NoError(t, json.Unmarshal([]byte(`{"id":1,"name":"acc","status":"active"}`), &a))
+ require.Nil(t, a.RateMultiplier)
+ require.Equal(t, 1.0, a.BillingRateMultiplier())
+}
+
+func TestAccount_BillingRateMultiplier_AllowsZero(t *testing.T) {
+ v := 0.0
+ a := Account{RateMultiplier: &v}
+ require.Equal(t, 0.0, a.BillingRateMultiplier())
+}
+
+func TestAccount_BillingRateMultiplier_NegativeFallsBackToOne(t *testing.T) {
+ v := -1.0
+ a := Account{RateMultiplier: &v}
+ require.Equal(t, 1.0, a.BillingRateMultiplier())
+}
diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go
index 2f138b81..2badc760 100644
--- a/backend/internal/service/account_service.go
+++ b/backend/internal/service/account_service.go
@@ -63,14 +63,15 @@ type AccountRepository interface {
// AccountBulkUpdate describes the fields that can be updated in a bulk operation.
// Nil pointers mean "do not change".
type AccountBulkUpdate struct {
- Name *string
- ProxyID *int64
- Concurrency *int
- Priority *int
- Status *string
- Schedulable *bool
- Credentials map[string]any
- Extra map[string]any
+ Name *string
+ ProxyID *int64
+ Concurrency *int
+ Priority *int
+ RateMultiplier *float64
+ Status *string
+ Schedulable *bool
+ Credentials map[string]any
+ Extra map[string]any
}
// CreateAccountRequest 创建账号请求
diff --git a/backend/internal/service/account_usage_service.go b/backend/internal/service/account_usage_service.go
index f1ee43d2..469abc0f 100644
--- a/backend/internal/service/account_usage_service.go
+++ b/backend/internal/service/account_usage_service.go
@@ -96,10 +96,16 @@ func NewUsageCache() *UsageCache {
}
// WindowStats 窗口期统计
+//
+// cost: 账号口径费用(total_cost * account_rate_multiplier)
+// standard_cost: 标准费用(total_cost,不含倍率)
+// user_cost: 用户/API Key 口径费用(actual_cost,受分组倍率影响)
type WindowStats struct {
- Requests int64 `json:"requests"`
- Tokens int64 `json:"tokens"`
- Cost float64 `json:"cost"`
+ Requests int64 `json:"requests"`
+ Tokens int64 `json:"tokens"`
+ Cost float64 `json:"cost"`
+ StandardCost float64 `json:"standard_cost"`
+ UserCost float64 `json:"user_cost"`
}
// UsageProgress 使用量进度
@@ -377,9 +383,11 @@ func (s *AccountUsageService) addWindowStats(ctx context.Context, account *Accou
}
windowStats = &WindowStats{
- Requests: stats.Requests,
- Tokens: stats.Tokens,
- Cost: stats.Cost,
+ Requests: stats.Requests,
+ Tokens: stats.Tokens,
+ Cost: stats.Cost,
+ StandardCost: stats.StandardCost,
+ UserCost: stats.UserCost,
}
// 缓存窗口统计(1 分钟)
@@ -403,9 +411,11 @@ func (s *AccountUsageService) GetTodayStats(ctx context.Context, accountID int64
}
return &WindowStats{
- Requests: stats.Requests,
- Tokens: stats.Tokens,
- Cost: stats.Cost,
+ Requests: stats.Requests,
+ Tokens: stats.Tokens,
+ Cost: stats.Cost,
+ StandardCost: stats.StandardCost,
+ UserCost: stats.UserCost,
}, nil
}
diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go
index 1874c5c1..8f64154e 100644
--- a/backend/internal/service/admin_service.go
+++ b/backend/internal/service/admin_service.go
@@ -136,6 +136,7 @@ type CreateAccountInput struct {
ProxyID *int64
Concurrency int
Priority int
+ RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
GroupIDs []int64
ExpiresAt *int64
AutoPauseOnExpired *bool
@@ -151,8 +152,9 @@ type UpdateAccountInput struct {
Credentials map[string]any
Extra map[string]any
ProxyID *int64
- Concurrency *int // 使用指针区分"未提供"和"设置为0"
- Priority *int // 使用指针区分"未提供"和"设置为0"
+ Concurrency *int // 使用指针区分"未提供"和"设置为0"
+ Priority *int // 使用指针区分"未提供"和"设置为0"
+ RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
Status string
GroupIDs *[]int64
ExpiresAt *int64
@@ -162,16 +164,17 @@ type UpdateAccountInput struct {
// BulkUpdateAccountsInput describes the payload for bulk updating accounts.
type BulkUpdateAccountsInput struct {
- AccountIDs []int64
- Name string
- ProxyID *int64
- Concurrency *int
- Priority *int
- Status string
- Schedulable *bool
- GroupIDs *[]int64
- Credentials map[string]any
- Extra map[string]any
+ AccountIDs []int64
+ Name string
+ ProxyID *int64
+ Concurrency *int
+ Priority *int
+ RateMultiplier *float64 // 账号计费倍率(>=0,允许 0)
+ Status string
+ Schedulable *bool
+ GroupIDs *[]int64
+ Credentials map[string]any
+ Extra map[string]any
// SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
// This should only be set when the caller has explicitly confirmed the risk.
SkipMixedChannelCheck bool
@@ -817,6 +820,12 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
} else {
account.AutoPauseOnExpired = true
}
+ if input.RateMultiplier != nil {
+ if *input.RateMultiplier < 0 {
+ return nil, errors.New("rate_multiplier must be >= 0")
+ }
+ account.RateMultiplier = input.RateMultiplier
+ }
if err := s.accountRepo.Create(ctx, account); err != nil {
return nil, err
}
@@ -869,6 +878,12 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
if input.Priority != nil {
account.Priority = *input.Priority
}
+ if input.RateMultiplier != nil {
+ if *input.RateMultiplier < 0 {
+ return nil, errors.New("rate_multiplier must be >= 0")
+ }
+ account.RateMultiplier = input.RateMultiplier
+ }
if input.Status != "" {
account.Status = input.Status
}
@@ -942,6 +957,12 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
}
}
+ if input.RateMultiplier != nil {
+ if *input.RateMultiplier < 0 {
+ return nil, errors.New("rate_multiplier must be >= 0")
+ }
+ }
+
// Prepare bulk updates for columns and JSONB fields.
repoUpdates := AccountBulkUpdate{
Credentials: input.Credentials,
@@ -959,6 +980,9 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp
if input.Priority != nil {
repoUpdates.Priority = input.Priority
}
+ if input.RateMultiplier != nil {
+ repoUpdates.RateMultiplier = input.RateMultiplier
+ }
if input.Status != "" {
repoUpdates.Status = &input.Status
}
diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go
index 2a5c44c6..436f5662 100644
--- a/backend/internal/service/gateway_service.go
+++ b/backend/internal/service/gateway_service.go
@@ -2618,30 +2618,32 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu
if result.ImageSize != "" {
imageSize = &result.ImageSize
}
+ accountRateMultiplier := account.BillingRateMultiplier()
usageLog := &UsageLog{
- UserID: user.ID,
- APIKeyID: apiKey.ID,
- AccountID: account.ID,
- RequestID: result.RequestID,
- Model: result.Model,
- InputTokens: result.Usage.InputTokens,
- OutputTokens: result.Usage.OutputTokens,
- CacheCreationTokens: result.Usage.CacheCreationInputTokens,
- CacheReadTokens: result.Usage.CacheReadInputTokens,
- InputCost: cost.InputCost,
- OutputCost: cost.OutputCost,
- CacheCreationCost: cost.CacheCreationCost,
- CacheReadCost: cost.CacheReadCost,
- TotalCost: cost.TotalCost,
- ActualCost: cost.ActualCost,
- RateMultiplier: multiplier,
- BillingType: billingType,
- Stream: result.Stream,
- DurationMs: &durationMs,
- FirstTokenMs: result.FirstTokenMs,
- ImageCount: result.ImageCount,
- ImageSize: imageSize,
- CreatedAt: time.Now(),
+ UserID: user.ID,
+ APIKeyID: apiKey.ID,
+ AccountID: account.ID,
+ RequestID: result.RequestID,
+ Model: result.Model,
+ InputTokens: result.Usage.InputTokens,
+ OutputTokens: result.Usage.OutputTokens,
+ CacheCreationTokens: result.Usage.CacheCreationInputTokens,
+ CacheReadTokens: result.Usage.CacheReadInputTokens,
+ InputCost: cost.InputCost,
+ OutputCost: cost.OutputCost,
+ CacheCreationCost: cost.CacheCreationCost,
+ CacheReadCost: cost.CacheReadCost,
+ TotalCost: cost.TotalCost,
+ ActualCost: cost.ActualCost,
+ RateMultiplier: multiplier,
+ AccountRateMultiplier: &accountRateMultiplier,
+ BillingType: billingType,
+ Stream: result.Stream,
+ DurationMs: &durationMs,
+ FirstTokenMs: result.FirstTokenMs,
+ ImageCount: result.ImageCount,
+ ImageSize: imageSize,
+ CreatedAt: time.Now(),
}
// 添加 UserAgent
diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go
index bac117b8..8d3b8137 100644
--- a/backend/internal/service/openai_gateway_service.go
+++ b/backend/internal/service/openai_gateway_service.go
@@ -1432,28 +1432,30 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec
// Create usage log
durationMs := int(result.Duration.Milliseconds())
+ accountRateMultiplier := account.BillingRateMultiplier()
usageLog := &UsageLog{
- UserID: user.ID,
- APIKeyID: apiKey.ID,
- AccountID: account.ID,
- RequestID: result.RequestID,
- Model: result.Model,
- InputTokens: actualInputTokens,
- OutputTokens: result.Usage.OutputTokens,
- CacheCreationTokens: result.Usage.CacheCreationInputTokens,
- CacheReadTokens: result.Usage.CacheReadInputTokens,
- InputCost: cost.InputCost,
- OutputCost: cost.OutputCost,
- CacheCreationCost: cost.CacheCreationCost,
- CacheReadCost: cost.CacheReadCost,
- TotalCost: cost.TotalCost,
- ActualCost: cost.ActualCost,
- RateMultiplier: multiplier,
- BillingType: billingType,
- Stream: result.Stream,
- DurationMs: &durationMs,
- FirstTokenMs: result.FirstTokenMs,
- CreatedAt: time.Now(),
+ UserID: user.ID,
+ APIKeyID: apiKey.ID,
+ AccountID: account.ID,
+ RequestID: result.RequestID,
+ Model: result.Model,
+ InputTokens: actualInputTokens,
+ OutputTokens: result.Usage.OutputTokens,
+ CacheCreationTokens: result.Usage.CacheCreationInputTokens,
+ CacheReadTokens: result.Usage.CacheReadInputTokens,
+ InputCost: cost.InputCost,
+ OutputCost: cost.OutputCost,
+ CacheCreationCost: cost.CacheCreationCost,
+ CacheReadCost: cost.CacheReadCost,
+ TotalCost: cost.TotalCost,
+ ActualCost: cost.ActualCost,
+ RateMultiplier: multiplier,
+ AccountRateMultiplier: &accountRateMultiplier,
+ BillingType: billingType,
+ Stream: result.Stream,
+ DurationMs: &durationMs,
+ FirstTokenMs: result.FirstTokenMs,
+ CreatedAt: time.Now(),
}
// 添加 UserAgent
diff --git a/backend/internal/service/usage_log.go b/backend/internal/service/usage_log.go
index 62d7fae0..3b0e934f 100644
--- a/backend/internal/service/usage_log.go
+++ b/backend/internal/service/usage_log.go
@@ -33,6 +33,8 @@ type UsageLog struct {
TotalCost float64
ActualCost float64
RateMultiplier float64
+ // AccountRateMultiplier 账号计费倍率快照(nil 表示历史数据,按 1.0 处理)
+ AccountRateMultiplier *float64
BillingType int8
Stream bool
diff --git a/backend/migrations/037_add_account_rate_multiplier.sql b/backend/migrations/037_add_account_rate_multiplier.sql
new file mode 100644
index 00000000..06f5b090
--- /dev/null
+++ b/backend/migrations/037_add_account_rate_multiplier.sql
@@ -0,0 +1,14 @@
+-- Add account billing rate multiplier and per-usage snapshot.
+--
+-- accounts.rate_multiplier: 账号计费倍率(>=0,允许 0 表示该账号计费为 0)。
+-- usage_logs.account_rate_multiplier: 每条 usage log 的账号倍率快照,用于实现
+-- “倍率调整仅影响之后请求”,并支持同一天分段倍率加权统计。
+--
+-- 注意:usage_logs.account_rate_multiplier 不做回填、不设置 NOT NULL。
+-- 老数据为 NULL 时,统计口径按 1.0 处理(COALESCE)。
+
+ALTER TABLE IF EXISTS accounts
+ ADD COLUMN IF NOT EXISTS rate_multiplier DECIMAL(10,4) NOT NULL DEFAULT 1.0;
+
+ALTER TABLE IF EXISTS usage_logs
+ ADD COLUMN IF NOT EXISTS account_rate_multiplier DECIMAL(10,4);
diff --git a/frontend/src/api/admin/usage.ts b/frontend/src/api/admin/usage.ts
index ca76234b..dd85fc24 100644
--- a/frontend/src/api/admin/usage.ts
+++ b/frontend/src/api/admin/usage.ts
@@ -16,6 +16,7 @@ export interface AdminUsageStatsResponse {
total_tokens: number
total_cost: number
total_actual_cost: number
+ total_account_cost?: number
average_duration_ms: number
}
diff --git a/frontend/src/components/account/AccountStatsModal.vue b/frontend/src/components/account/AccountStatsModal.vue
index 92016699..7968fa8d 100644
--- a/frontend/src/components/account/AccountStatsModal.vue
+++ b/frontend/src/components/account/AccountStatsModal.vue
@@ -73,11 +73,12 @@
{{ t('admin.accounts.stats.accumulatedCost') }}
- ({{ t('admin.accounts.stats.standardCost') }}: ${{
+
+ ({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.total_user_cost) }} ·
+ {{ t('admin.accounts.stats.standardCost') }}: ${{
formatCost(stats.summary.total_standard_cost)
- }})
+ }})
+
@@ -121,12 +122,15 @@
${{ formatCost(stats.summary.avg_daily_cost) }}
-
+
{{
t('admin.accounts.stats.basedOnActualDays', {
days: stats.summary.actual_days_used
})
}}
+
+ ({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.avg_daily_user_cost) }})
+
@@ -189,13 +193,17 @@
- {{
- t('admin.accounts.stats.cost')
- }}
+ {{ t('usage.accountBilled') }}
${{ formatCost(stats.summary.today?.cost || 0) }}
+
+ {{ t('usage.userBilled') }}
+ ${{ formatCost(stats.summary.today?.user_cost || 0) }}
+
{{
t('admin.accounts.stats.requests')
@@ -240,13 +248,17 @@
}}
- {{
- t('admin.accounts.stats.cost')
- }}
+ {{ t('usage.accountBilled') }}
${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}
+
+ {{ t('usage.userBilled') }}
+ ${{ formatCost(stats.summary.highest_cost_day?.user_cost || 0) }}
+
{{
t('admin.accounts.stats.requests')
@@ -291,13 +303,17 @@
}}
- {{
- t('admin.accounts.stats.cost')
- }}
+ {{ t('usage.accountBilled') }}
${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}
+
+ {{ t('usage.userBilled') }}
+ ${{ formatCost(stats.summary.highest_request_day?.user_cost || 0) }}
+
@@ -397,13 +413,17 @@
}}
- {{
- t('admin.accounts.stats.todayCost')
- }}
+ {{ t('usage.accountBilled') }}
${{ formatCost(stats.summary.today?.cost || 0) }}
+
+ {{ t('usage.userBilled') }}
+ ${{ formatCost(stats.summary.today?.user_cost || 0) }}
+
@@ -517,14 +537,24 @@ const trendChartData = computed(() => {
labels: stats.value.history.map((h) => h.label),
datasets: [
{
- label: t('admin.accounts.stats.cost') + ' (USD)',
- data: stats.value.history.map((h) => h.cost),
+ label: t('usage.accountBilled') + ' (USD)',
+ data: stats.value.history.map((h) => h.actual_cost),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.3,
yAxisID: 'y'
},
+ {
+ label: t('usage.userBilled') + ' (USD)',
+ data: stats.value.history.map((h) => h.user_cost),
+ borderColor: '#10b981',
+ backgroundColor: 'rgba(16, 185, 129, 0.08)',
+ fill: false,
+ tension: 0.3,
+ borderDash: [5, 5],
+ yAxisID: 'y'
+ },
{
label: t('admin.accounts.stats.requests'),
data: stats.value.history.map((h) => h.requests),
@@ -602,7 +632,7 @@ const lineChartOptions = computed(() => ({
},
title: {
display: true,
- text: t('admin.accounts.stats.cost') + ' (USD)',
+ text: t('usage.accountBilled') + ' (USD)',
color: '#3b82f6',
font: {
size: 11
diff --git a/frontend/src/components/account/AccountTodayStatsCell.vue b/frontend/src/components/account/AccountTodayStatsCell.vue
index b8bbc618..a920f314 100644
--- a/frontend/src/components/account/AccountTodayStatsCell.vue
+++ b/frontend/src/components/account/AccountTodayStatsCell.vue
@@ -32,15 +32,20 @@
formatTokens(stats.tokens)
}}
-
+
- {{ t('admin.accounts.stats.cost') }}:
+ {{ t('usage.accountBilled') }}:
{{
formatCurrency(stats.cost)
}}
+
+
+ {{ t('usage.userBilled') }}:
+ {{
+ formatCurrency(stats.user_cost)
+ }}
+
diff --git a/frontend/src/components/account/BulkEditAccountModal.vue b/frontend/src/components/account/BulkEditAccountModal.vue
index 9ccf6130..fb776e96 100644
--- a/frontend/src/components/account/BulkEditAccountModal.vue
+++ b/frontend/src/components/account/BulkEditAccountModal.vue
@@ -459,7 +459,7 @@
-
+
+
+
+
+
+
+
+
{{ t('admin.accounts.billingRateMultiplierHint') }}
+
@@ -655,6 +685,7 @@ const enableInterceptWarmup = ref(false)
const enableProxy = ref(false)
const enableConcurrency = ref(false)
const enablePriority = ref(false)
+const enableRateMultiplier = ref(false)
const enableStatus = ref(false)
const enableGroups = ref(false)
@@ -670,6 +701,7 @@ const interceptWarmupRequests = ref(false)
const proxyId = ref
(null)
const concurrency = ref(1)
const priority = ref(1)
+const rateMultiplier = ref(1)
const status = ref<'active' | 'inactive'>('active')
const groupIds = ref([])
@@ -863,6 +895,10 @@ const buildUpdatePayload = (): Record | null => {
updates.priority = priority.value
}
+ if (enableRateMultiplier.value) {
+ updates.rate_multiplier = rateMultiplier.value
+ }
+
if (enableStatus.value) {
updates.status = status.value
}
@@ -923,6 +959,7 @@ const handleSubmit = async () => {
enableProxy.value ||
enableConcurrency.value ||
enablePriority.value ||
+ enableRateMultiplier.value ||
enableStatus.value ||
enableGroups.value
@@ -977,6 +1014,7 @@ watch(
enableProxy.value = false
enableConcurrency.value = false
enablePriority.value = false
+ enableRateMultiplier.value = false
enableStatus.value = false
enableGroups.value = false
@@ -991,6 +1029,7 @@ watch(
proxyId.value = null
concurrency.value = 1
priority.value = 1
+ rateMultiplier.value = 1
status.value = 'active'
groupIds.value = []
}
diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue
index a56a987f..c81de00e 100644
--- a/frontend/src/components/account/CreateAccountModal.vue
+++ b/frontend/src/components/account/CreateAccountModal.vue
@@ -1196,7 +1196,7 @@
-
+
+
+
+
+
{{ t('admin.accounts.billingRateMultiplierHint') }}
+
@@ -1832,6 +1837,7 @@ const form = reactive({
proxy_id: null as number | null,
concurrency: 10,
priority: 1,
+ rate_multiplier: 1,
group_ids: [] as number[],
expires_at: null as number | null
})
@@ -2119,6 +2125,7 @@ const resetForm = () => {
form.proxy_id = null
form.concurrency = 10
form.priority = 1
+ form.rate_multiplier = 1
form.group_ids = []
form.expires_at = null
accountCategory.value = 'oauth-based'
@@ -2272,6 +2279,7 @@ const createAccountAndFinish = async (
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
+ rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
@@ -2490,6 +2498,7 @@ const handleCookieAuth = async (sessionKey: string) => {
proxy_id: form.proxy_id,
concurrency: form.concurrency,
priority: form.priority,
+ rate_multiplier: form.rate_multiplier,
group_ids: form.group_ids,
expires_at: form.expires_at,
auto_pause_on_expired: autoPauseOnExpired.value
diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue
index 7cb740bd..00cd9b24 100644
--- a/frontend/src/components/account/EditAccountModal.vue
+++ b/frontend/src/components/account/EditAccountModal.vue
@@ -549,7 +549,7 @@
-
+
@@ -564,6 +564,11 @@
data-tour="account-form-priority"
/>
+
+
+
+
{{ t('admin.accounts.billingRateMultiplierHint') }}
+
@@ -807,6 +812,7 @@ const form = reactive({
proxy_id: null as number | null,
concurrency: 1,
priority: 1,
+ rate_multiplier: 1,
status: 'active' as 'active' | 'inactive',
group_ids: [] as number[],
expires_at: null as number | null
@@ -834,6 +840,7 @@ watch(
form.proxy_id = newAccount.proxy_id
form.concurrency = newAccount.concurrency
form.priority = newAccount.priority
+ form.rate_multiplier = newAccount.rate_multiplier ?? 1
form.status = newAccount.status as 'active' | 'inactive'
form.group_ids = newAccount.group_ids || []
form.expires_at = newAccount.expires_at ?? null
diff --git a/frontend/src/components/account/UsageProgressBar.vue b/frontend/src/components/account/UsageProgressBar.vue
index 1b3561ef..93844295 100644
--- a/frontend/src/components/account/UsageProgressBar.vue
+++ b/frontend/src/components/account/UsageProgressBar.vue
@@ -15,7 +15,13 @@
{{ formatTokens }}
- ${{ formatCost }}
+ A ${{ formatAccountCost }}
+
+ U ${{ formatUserCost }}
+
@@ -149,8 +155,13 @@ const formatTokens = computed(() => {
return t.toString()
})
-const formatCost = computed(() => {
+const formatAccountCost = computed(() => {
if (!props.windowStats) return '0.00'
return props.windowStats.cost.toFixed(2)
})
+
+const formatUserCost = computed(() => {
+ if (!props.windowStats || props.windowStats.user_cost == null) return '0.00'
+ return props.windowStats.user_cost.toFixed(2)
+})
diff --git a/frontend/src/components/admin/account/AccountStatsModal.vue b/frontend/src/components/admin/account/AccountStatsModal.vue
index 138f5811..72a71d36 100644
--- a/frontend/src/components/admin/account/AccountStatsModal.vue
+++ b/frontend/src/components/admin/account/AccountStatsModal.vue
@@ -61,11 +61,12 @@
{{ t('admin.accounts.stats.accumulatedCost') }}
- ({{ t('admin.accounts.stats.standardCost') }}: ${{
+
+ ({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.total_user_cost) }} ·
+ {{ t('admin.accounts.stats.standardCost') }}: ${{
formatCost(stats.summary.total_standard_cost)
- }})
+ }})
+
@@ -108,12 +109,15 @@
${{ formatCost(stats.summary.avg_daily_cost) }}
-
+
{{
t('admin.accounts.stats.basedOnActualDays', {
days: stats.summary.actual_days_used
})
}}
+
+ ({{ t('usage.userBilled') }}: ${{ formatCost(stats.summary.avg_daily_user_cost) }})
+
@@ -164,13 +168,17 @@
- {{
- t('admin.accounts.stats.cost')
- }}
+ {{ t('usage.accountBilled') }}
${{ formatCost(stats.summary.today?.cost || 0) }}
+
+ {{ t('usage.userBilled') }}
+ ${{ formatCost(stats.summary.today?.user_cost || 0) }}
+
{{
t('admin.accounts.stats.requests')
@@ -210,13 +218,17 @@
}}
- {{
- t('admin.accounts.stats.cost')
- }}
+ {{ t('usage.accountBilled') }}
${{ formatCost(stats.summary.highest_cost_day?.cost || 0) }}
+
+ {{ t('usage.userBilled') }}
+ ${{ formatCost(stats.summary.highest_cost_day?.user_cost || 0) }}
+
{{
t('admin.accounts.stats.requests')
@@ -260,13 +272,17 @@
}}
- {{
- t('admin.accounts.stats.cost')
- }}
+ {{ t('usage.accountBilled') }}
${{ formatCost(stats.summary.highest_request_day?.cost || 0) }}
+
+ {{ t('usage.userBilled') }}
+ ${{ formatCost(stats.summary.highest_request_day?.user_cost || 0) }}
+
@@ -485,14 +501,24 @@ const trendChartData = computed(() => {
labels: stats.value.history.map((h) => h.label),
datasets: [
{
- label: t('admin.accounts.stats.cost') + ' (USD)',
- data: stats.value.history.map((h) => h.cost),
+ label: t('usage.accountBilled') + ' (USD)',
+ data: stats.value.history.map((h) => h.actual_cost),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
tension: 0.3,
yAxisID: 'y'
},
+ {
+ label: t('usage.userBilled') + ' (USD)',
+ data: stats.value.history.map((h) => h.user_cost),
+ borderColor: '#10b981',
+ backgroundColor: 'rgba(16, 185, 129, 0.08)',
+ fill: false,
+ tension: 0.3,
+ borderDash: [5, 5],
+ yAxisID: 'y'
+ },
{
label: t('admin.accounts.stats.requests'),
data: stats.value.history.map((h) => h.requests),
@@ -570,7 +596,7 @@ const lineChartOptions = computed(() => ({
},
title: {
display: true,
- text: t('admin.accounts.stats.cost') + ' (USD)',
+ text: t('usage.accountBilled') + ' (USD)',
color: '#3b82f6',
font: {
size: 11
diff --git a/frontend/src/components/admin/usage/UsageStatsCards.vue b/frontend/src/components/admin/usage/UsageStatsCards.vue
index 16ce6619..cd962a09 100644
--- a/frontend/src/components/admin/usage/UsageStatsCards.vue
+++ b/frontend/src/components/admin/usage/UsageStatsCards.vue
@@ -27,9 +27,18 @@
{{ t('usage.totalCost') }}
-
${{ (stats?.total_actual_cost || 0).toFixed(4) }}
-
- {{ t('usage.standardCost') }}: ${{ (stats?.total_cost || 0).toFixed(4) }}
+
+ ${{ ((stats?.total_account_cost ?? stats?.total_actual_cost) || 0).toFixed(4) }}
+
+
+ {{ t('usage.userBilled') }}:
+ ${{ (stats?.total_actual_cost || 0).toFixed(4) }}
+ · {{ t('usage.standardCost') }}:
+ ${{ (stats?.total_cost || 0).toFixed(4) }}
+
+
+ {{ t('usage.standardCost') }}:
+ ${{ (stats?.total_cost || 0).toFixed(4) }}
diff --git a/frontend/src/components/admin/usage/UsageTable.vue b/frontend/src/components/admin/usage/UsageTable.vue
index a66e4b7b..d2260c59 100644
--- a/frontend/src/components/admin/usage/UsageTable.vue
+++ b/frontend/src/components/admin/usage/UsageTable.vue
@@ -81,18 +81,23 @@
-
-
${{ row.actual_cost?.toFixed(6) || '0.000000' }}
-
-
-
-
+
+
+
${{ row.actual_cost?.toFixed(6) || '0.000000' }}
+
+
+
+ A ${{ (row.total_cost * row.account_rate_multiplier).toFixed(6) }}
+
@@ -202,14 +207,24 @@
{{ t('usage.rate') }}
{{ (tooltipData?.rate_multiplier || 1).toFixed(2) }}x
+
+ {{ t('usage.accountMultiplier') }}
+ {{ (tooltipData?.account_rate_multiplier ?? 1).toFixed(2) }}x
+
{{ t('usage.original') }}
${{ tooltipData?.total_cost?.toFixed(6) || '0.000000' }}
-
-
{{ t('usage.billed') }}
+
+ {{ t('usage.userBilled') }}
${{ tooltipData?.actual_cost?.toFixed(6) || '0.000000' }}
+
+ {{ t('usage.accountBilled') }}
+
+ ${{ (((tooltipData?.total_cost || 0) * (tooltipData?.account_rate_multiplier ?? 1)) || 0).toFixed(6) }}
+
+
diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts
index bd17a7f1..bb2c05bc 100644
--- a/frontend/src/i18n/locales/en.ts
+++ b/frontend/src/i18n/locales/en.ts
@@ -429,6 +429,9 @@ export default {
totalCost: 'Total Cost',
standardCost: 'Standard',
actualCost: 'Actual',
+ userBilled: 'User billed',
+ accountBilled: 'Account billed',
+ accountMultiplier: 'Account rate',
avgDuration: 'Avg Duration',
inSelectedRange: 'in selected range',
perRequest: 'per request',
@@ -1058,6 +1061,7 @@ export default {
concurrencyStatus: 'Concurrency',
notes: 'Notes',
priority: 'Priority',
+ billingRateMultiplier: 'Billing Rate',
weight: 'Weight',
status: 'Status',
schedulable: 'Schedulable',
@@ -1225,6 +1229,8 @@ export default {
concurrency: 'Concurrency',
priority: 'Priority',
priorityHint: 'Lower value accounts are used first',
+ billingRateMultiplier: 'Billing Rate Multiplier',
+ billingRateMultiplierHint: '>=0, 0 means free. Affects account billing only',
expiresAt: 'Expires At',
expiresAtHint: 'Leave empty for no expiration',
higherPriorityFirst: 'Lower value means higher priority',
diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts
index 9724a55c..dcb0a812 100644
--- a/frontend/src/i18n/locales/zh.ts
+++ b/frontend/src/i18n/locales/zh.ts
@@ -426,6 +426,9 @@ export default {
totalCost: '总消费',
standardCost: '标准',
actualCost: '实际',
+ userBilled: '用户扣费',
+ accountBilled: '账号计费',
+ accountMultiplier: '账号倍率',
avgDuration: '平均耗时',
inSelectedRange: '所选范围内',
perRequest: '每次请求',
@@ -1108,6 +1111,7 @@ export default {
concurrencyStatus: '并发',
notes: '备注',
priority: '优先级',
+ billingRateMultiplier: '账号倍率',
weight: '权重',
status: '状态',
schedulable: '调度',
@@ -1359,6 +1363,8 @@ export default {
concurrency: '并发数',
priority: '优先级',
priorityHint: '优先级越小的账号优先使用',
+ billingRateMultiplier: '账号计费倍率',
+ billingRateMultiplierHint: '>=0,0 表示该账号计费为 0;仅影响账号计费口径',
expiresAt: '过期时间',
expiresAtHint: '留空表示不过期',
higherPriorityFirst: '数值越小优先级越高',
diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts
index 5eb74596..66f71b7e 100644
--- a/frontend/src/types/index.ts
+++ b/frontend/src/types/index.ts
@@ -428,6 +428,7 @@ export interface Account {
concurrency: number
current_concurrency?: number // Real-time concurrency count from Redis
priority: number
+ rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
status: 'active' | 'inactive' | 'error'
error_message: string | null
last_used_at: string | null
@@ -457,7 +458,9 @@ export interface Account {
export interface WindowStats {
requests: number
tokens: number
- cost: number
+ cost: number // Account cost (account multiplier)
+ standard_cost?: number
+ user_cost?: number
}
export interface UsageProgress {
@@ -522,6 +525,7 @@ export interface CreateAccountRequest {
proxy_id?: number | null
concurrency?: number
priority?: number
+ rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
group_ids?: number[]
expires_at?: number | null
auto_pause_on_expired?: boolean
@@ -537,6 +541,7 @@ export interface UpdateAccountRequest {
proxy_id?: number | null
concurrency?: number
priority?: number
+ rate_multiplier?: number // Account billing multiplier (>=0, 0 means free)
schedulable?: boolean
status?: 'active' | 'inactive'
group_ids?: number[]
@@ -593,6 +598,7 @@ export interface UsageLog {
total_cost: number
actual_cost: number
rate_multiplier: number
+ account_rate_multiplier?: number | null
stream: boolean
duration_ms: number
@@ -852,23 +858,27 @@ export interface AccountUsageHistory {
requests: number
tokens: number
cost: number
- actual_cost: number
+ actual_cost: number // Account cost (account multiplier)
+ user_cost: number // User/API key billed cost (group multiplier)
}
export interface AccountUsageSummary {
days: number
actual_days_used: number
- total_cost: number
+ total_cost: number // Account cost (account multiplier)
+ total_user_cost: number
total_standard_cost: number
total_requests: number
total_tokens: number
- avg_daily_cost: number
+ avg_daily_cost: number // Account cost
+ avg_daily_user_cost: number
avg_daily_requests: number
avg_daily_tokens: number
avg_duration_ms: number
today: {
date: string
cost: number
+ user_cost: number
requests: number
tokens: number
} | null
@@ -876,6 +886,7 @@ export interface AccountUsageSummary {
date: string
label: string
cost: number
+ user_cost: number
requests: number
} | null
highest_request_day: {
@@ -883,6 +894,7 @@ export interface AccountUsageSummary {
label: string
requests: number
cost: number
+ user_cost: number
} | null
}
diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue
index 8a5268ca..ce9e9125 100644
--- a/frontend/src/views/admin/AccountsView.vue
+++ b/frontend/src/views/admin/AccountsView.vue
@@ -61,6 +61,11 @@
+
+
+ {{ (row.rate_multiplier ?? 1).toFixed(2) }}x
+
+
{{ value }}
@@ -190,10 +195,11 @@ const cols = computed(() => {
if (!authStore.isSimpleMode) {
c.push({ key: 'groups', label: t('admin.accounts.columns.groups'), sortable: false })
}
- c.push(
- { key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
- { key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
- { key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
+ c.push(
+ { key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
+ { key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
+ { key: 'rate_multiplier', label: t('admin.accounts.columns.billingRateMultiplier'), sortable: true },
+ { key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
{ key: 'expires_at', label: t('admin.accounts.columns.expiresAt'), sortable: true },
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
diff --git a/frontend/src/views/admin/UsageView.vue b/frontend/src/views/admin/UsageView.vue
index fbde13fd..749e9dbd 100644
--- a/frontend/src/views/admin/UsageView.vue
+++ b/frontend/src/views/admin/UsageView.vue
@@ -94,7 +94,7 @@ const exportToExcel = async () => {
t('admin.usage.cacheReadTokens'), t('admin.usage.cacheCreationTokens'),
t('admin.usage.inputCost'), t('admin.usage.outputCost'),
t('admin.usage.cacheReadCost'), t('admin.usage.cacheCreationCost'),
- t('usage.rate'), t('usage.original'), t('usage.billed'),
+ t('usage.rate'), t('usage.accountMultiplier'), t('usage.original'), t('usage.userBilled'), t('usage.accountBilled'),
t('usage.firstToken'), t('usage.duration'),
t('admin.usage.requestId'), t('usage.userAgent'), t('admin.usage.ipAddress')
]
@@ -115,8 +115,10 @@ const exportToExcel = async () => {
log.cache_read_cost?.toFixed(6) || '0.000000',
log.cache_creation_cost?.toFixed(6) || '0.000000',
log.rate_multiplier?.toFixed(2) || '1.00',
+ (log.account_rate_multiplier ?? 1).toFixed(2),
log.total_cost?.toFixed(6) || '0.000000',
log.actual_cost?.toFixed(6) || '0.000000',
+ (log.total_cost * (log.account_rate_multiplier ?? 1)).toFixed(6),
log.first_token_ms ?? '',
log.duration_ms,
log.request_id || '',