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.billingRateMultiplierHint') }}
+{{ t('admin.accounts.priorityHint') }}
{{ t('admin.accounts.billingRateMultiplierHint') }}
+{{ t('admin.accounts.billingRateMultiplierHint') }}
+{{ 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) - }}) + }}) +
${{ 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) }}) +
{{ 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) }}