From f694afbbf431122407fe7f4bfb3d7f4ee0e5654f Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 12 Apr 2026 13:53:02 +0800 Subject: [PATCH] feat(notify): add percentage threshold type for balance low notification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add threshold_type field (fixed/percentage) to system and user settings - Add total_recharged field to users table, auto-incremented on balance credit - Percentage mode: effective threshold = total_recharged × percentage / 100 - User-level threshold_type inherits from system default when not set - Update admin settings UI with radio selector (fixed amount / percentage) - Migration: 102_add_balance_notify_threshold_type.sql --- backend/ent/migrate/schema.go | 2 + backend/ent/mutation.go | 143 ++++++++++++++++- backend/ent/runtime/runtime.go | 10 +- backend/ent/schema/user.go | 5 + backend/ent/user.go | 26 ++- backend/ent/user/user.go | 20 +++ backend/ent/user/where.go | 115 ++++++++++++++ backend/ent/user_create.go | 150 ++++++++++++++++++ backend/ent/user_update.go | 88 ++++++++++ .../internal/handler/admin/setting_handler.go | 38 +++++ backend/internal/handler/dto/mappers.go | 28 ++-- backend/internal/handler/dto/settings.go | 7 +- backend/internal/handler/dto/types.go | 8 +- backend/internal/handler/user_handler.go | 14 +- backend/internal/repository/api_key_repo.go | 34 ++-- backend/internal/repository/user_repo.go | 8 +- .../service/balance_notify_service.go | 51 ++++-- backend/internal/service/domain_constants.go | 9 +- backend/internal/service/setting_service.go | 9 ++ backend/internal/service/settings_view.go | 5 +- backend/internal/service/user.go | 8 +- backend/internal/service/user_service.go | 14 +- .../102_add_balance_notify_threshold_type.sql | 4 + frontend/src/api/admin/settings.ts | 2 + frontend/src/i18n/locales/en.ts | 4 + frontend/src/i18n/locales/zh.ts | 6 +- frontend/src/views/admin/SettingsView.vue | 104 +++++++++++- 27 files changed, 838 insertions(+), 74 deletions(-) create mode 100644 backend/migrations/102_add_balance_notify_threshold_type.sql diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index 4f31883b..1fff61ba 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -1079,8 +1079,10 @@ var ( {Name: "totp_enabled", Type: field.TypeBool, Default: false}, {Name: "totp_enabled_at", Type: field.TypeTime, Nullable: true}, {Name: "balance_notify_enabled", Type: field.TypeBool, Default: true}, + {Name: "balance_notify_threshold_type", Type: field.TypeString, Default: "fixed"}, {Name: "balance_notify_threshold", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "balance_notify_extra_emails", Type: field.TypeString, Default: "[]", SchemaType: map[string]string{"postgres": "text"}}, + {Name: "total_recharged", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, } // UsersTable holds the schema information for the "users" table. UsersTable = &schema.Table{ diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index cdaf363a..3bca248d 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -28211,9 +28211,12 @@ type UserMutation struct { totp_enabled *bool totp_enabled_at *time.Time balance_notify_enabled *bool + balance_notify_threshold_type *string balance_notify_threshold *float64 addbalance_notify_threshold *float64 balance_notify_extra_emails *string + total_recharged *float64 + addtotal_recharged *float64 clearedFields map[string]struct{} api_keys map[int64]struct{} removedapi_keys map[int64]struct{} @@ -28967,6 +28970,42 @@ func (m *UserMutation) ResetBalanceNotifyEnabled() { m.balance_notify_enabled = nil } +// SetBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field. +func (m *UserMutation) SetBalanceNotifyThresholdType(s string) { + m.balance_notify_threshold_type = &s +} + +// BalanceNotifyThresholdType returns the value of the "balance_notify_threshold_type" field in the mutation. +func (m *UserMutation) BalanceNotifyThresholdType() (r string, exists bool) { + v := m.balance_notify_threshold_type + if v == nil { + return + } + return *v, true +} + +// OldBalanceNotifyThresholdType returns the old "balance_notify_threshold_type" field's value of the User entity. +// If the User 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 *UserMutation) OldBalanceNotifyThresholdType(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldBalanceNotifyThresholdType is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldBalanceNotifyThresholdType requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldBalanceNotifyThresholdType: %w", err) + } + return oldValue.BalanceNotifyThresholdType, nil +} + +// ResetBalanceNotifyThresholdType resets all changes to the "balance_notify_threshold_type" field. +func (m *UserMutation) ResetBalanceNotifyThresholdType() { + m.balance_notify_threshold_type = nil +} + // SetBalanceNotifyThreshold sets the "balance_notify_threshold" field. func (m *UserMutation) SetBalanceNotifyThreshold(f float64) { m.balance_notify_threshold = &f @@ -29073,6 +29112,62 @@ func (m *UserMutation) ResetBalanceNotifyExtraEmails() { m.balance_notify_extra_emails = nil } +// SetTotalRecharged sets the "total_recharged" field. +func (m *UserMutation) SetTotalRecharged(f float64) { + m.total_recharged = &f + m.addtotal_recharged = nil +} + +// TotalRecharged returns the value of the "total_recharged" field in the mutation. +func (m *UserMutation) TotalRecharged() (r float64, exists bool) { + v := m.total_recharged + if v == nil { + return + } + return *v, true +} + +// OldTotalRecharged returns the old "total_recharged" field's value of the User entity. +// If the User 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 *UserMutation) OldTotalRecharged(ctx context.Context) (v float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldTotalRecharged is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldTotalRecharged requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldTotalRecharged: %w", err) + } + return oldValue.TotalRecharged, nil +} + +// AddTotalRecharged adds f to the "total_recharged" field. +func (m *UserMutation) AddTotalRecharged(f float64) { + if m.addtotal_recharged != nil { + *m.addtotal_recharged += f + } else { + m.addtotal_recharged = &f + } +} + +// AddedTotalRecharged returns the value that was added to the "total_recharged" field in this mutation. +func (m *UserMutation) AddedTotalRecharged() (r float64, exists bool) { + v := m.addtotal_recharged + if v == nil { + return + } + return *v, true +} + +// ResetTotalRecharged resets all changes to the "total_recharged" field. +func (m *UserMutation) ResetTotalRecharged() { + m.total_recharged = nil + m.addtotal_recharged = nil +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids. func (m *UserMutation) AddAPIKeyIDs(ids ...int64) { if m.api_keys == nil { @@ -29647,7 +29742,7 @@ func (m *UserMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *UserMutation) Fields() []string { - fields := make([]string, 0, 17) + fields := make([]string, 0, 19) if m.created_at != nil { fields = append(fields, user.FieldCreatedAt) } @@ -29693,12 +29788,18 @@ func (m *UserMutation) Fields() []string { if m.balance_notify_enabled != nil { fields = append(fields, user.FieldBalanceNotifyEnabled) } + if m.balance_notify_threshold_type != nil { + fields = append(fields, user.FieldBalanceNotifyThresholdType) + } if m.balance_notify_threshold != nil { fields = append(fields, user.FieldBalanceNotifyThreshold) } if m.balance_notify_extra_emails != nil { fields = append(fields, user.FieldBalanceNotifyExtraEmails) } + if m.total_recharged != nil { + fields = append(fields, user.FieldTotalRecharged) + } return fields } @@ -29737,10 +29838,14 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) { return m.TotpEnabledAt() case user.FieldBalanceNotifyEnabled: return m.BalanceNotifyEnabled() + case user.FieldBalanceNotifyThresholdType: + return m.BalanceNotifyThresholdType() case user.FieldBalanceNotifyThreshold: return m.BalanceNotifyThreshold() case user.FieldBalanceNotifyExtraEmails: return m.BalanceNotifyExtraEmails() + case user.FieldTotalRecharged: + return m.TotalRecharged() } return nil, false } @@ -29780,10 +29885,14 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldTotpEnabledAt(ctx) case user.FieldBalanceNotifyEnabled: return m.OldBalanceNotifyEnabled(ctx) + case user.FieldBalanceNotifyThresholdType: + return m.OldBalanceNotifyThresholdType(ctx) case user.FieldBalanceNotifyThreshold: return m.OldBalanceNotifyThreshold(ctx) case user.FieldBalanceNotifyExtraEmails: return m.OldBalanceNotifyExtraEmails(ctx) + case user.FieldTotalRecharged: + return m.OldTotalRecharged(ctx) } return nil, fmt.Errorf("unknown User field %s", name) } @@ -29898,6 +30007,13 @@ func (m *UserMutation) SetField(name string, value ent.Value) error { } m.SetBalanceNotifyEnabled(v) return nil + case user.FieldBalanceNotifyThresholdType: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBalanceNotifyThresholdType(v) + return nil case user.FieldBalanceNotifyThreshold: v, ok := value.(float64) if !ok { @@ -29912,6 +30028,13 @@ func (m *UserMutation) SetField(name string, value ent.Value) error { } m.SetBalanceNotifyExtraEmails(v) return nil + case user.FieldTotalRecharged: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetTotalRecharged(v) + return nil } return fmt.Errorf("unknown User field %s", name) } @@ -29929,6 +30052,9 @@ func (m *UserMutation) AddedFields() []string { if m.addbalance_notify_threshold != nil { fields = append(fields, user.FieldBalanceNotifyThreshold) } + if m.addtotal_recharged != nil { + fields = append(fields, user.FieldTotalRecharged) + } return fields } @@ -29943,6 +30069,8 @@ func (m *UserMutation) AddedField(name string) (ent.Value, bool) { return m.AddedConcurrency() case user.FieldBalanceNotifyThreshold: return m.AddedBalanceNotifyThreshold() + case user.FieldTotalRecharged: + return m.AddedTotalRecharged() } return nil, false } @@ -29973,6 +30101,13 @@ func (m *UserMutation) AddField(name string, value ent.Value) error { } m.AddBalanceNotifyThreshold(v) return nil + case user.FieldTotalRecharged: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddTotalRecharged(v) + return nil } return fmt.Errorf("unknown User numeric field %s", name) } @@ -30072,12 +30207,18 @@ func (m *UserMutation) ResetField(name string) error { case user.FieldBalanceNotifyEnabled: m.ResetBalanceNotifyEnabled() return nil + case user.FieldBalanceNotifyThresholdType: + m.ResetBalanceNotifyThresholdType() + return nil case user.FieldBalanceNotifyThreshold: m.ResetBalanceNotifyThreshold() return nil case user.FieldBalanceNotifyExtraEmails: m.ResetBalanceNotifyExtraEmails() return nil + case user.FieldTotalRecharged: + m.ResetTotalRecharged() + return nil } return fmt.Errorf("unknown User field %s", name) } diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index a288f5d9..951b5f99 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -1297,10 +1297,18 @@ func init() { userDescBalanceNotifyEnabled := userFields[11].Descriptor() // user.DefaultBalanceNotifyEnabled holds the default value on creation for the balance_notify_enabled field. user.DefaultBalanceNotifyEnabled = userDescBalanceNotifyEnabled.Default.(bool) + // userDescBalanceNotifyThresholdType is the schema descriptor for balance_notify_threshold_type field. + userDescBalanceNotifyThresholdType := userFields[12].Descriptor() + // user.DefaultBalanceNotifyThresholdType holds the default value on creation for the balance_notify_threshold_type field. + user.DefaultBalanceNotifyThresholdType = userDescBalanceNotifyThresholdType.Default.(string) // userDescBalanceNotifyExtraEmails is the schema descriptor for balance_notify_extra_emails field. - userDescBalanceNotifyExtraEmails := userFields[13].Descriptor() + userDescBalanceNotifyExtraEmails := userFields[14].Descriptor() // user.DefaultBalanceNotifyExtraEmails holds the default value on creation for the balance_notify_extra_emails field. user.DefaultBalanceNotifyExtraEmails = userDescBalanceNotifyExtraEmails.Default.(string) + // userDescTotalRecharged is the schema descriptor for total_recharged field. + userDescTotalRecharged := userFields[15].Descriptor() + // user.DefaultTotalRecharged holds the default value on creation for the total_recharged field. + user.DefaultTotalRecharged = userDescTotalRecharged.Default.(float64) userallowedgroupFields := schema.UserAllowedGroup{}.Fields() _ = userallowedgroupFields // userallowedgroupDescCreatedAt is the schema descriptor for created_at field. diff --git a/backend/ent/schema/user.go b/backend/ent/schema/user.go index bdaa4509..ef52e985 100644 --- a/backend/ent/schema/user.go +++ b/backend/ent/schema/user.go @@ -76,6 +76,8 @@ func (User) Fields() []ent.Field { // 余额不足通知 field.Bool("balance_notify_enabled"). Default(true), + field.String("balance_notify_threshold_type"). + Default("fixed"), // "fixed" | "percentage" field.Float("balance_notify_threshold"). SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}). Optional(). @@ -83,6 +85,9 @@ func (User) Fields() []ent.Field { field.String("balance_notify_extra_emails"). SchemaType(map[string]string{dialect.Postgres: "text"}). Default("[]"), + field.Float("total_recharged"). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}). + Default(0), } } diff --git a/backend/ent/user.go b/backend/ent/user.go index fc4ddb8f..9fa91f74 100644 --- a/backend/ent/user.go +++ b/backend/ent/user.go @@ -47,10 +47,14 @@ type User struct { TotpEnabledAt *time.Time `json:"totp_enabled_at,omitempty"` // BalanceNotifyEnabled holds the value of the "balance_notify_enabled" field. BalanceNotifyEnabled bool `json:"balance_notify_enabled,omitempty"` + // BalanceNotifyThresholdType holds the value of the "balance_notify_threshold_type" field. + BalanceNotifyThresholdType string `json:"balance_notify_threshold_type,omitempty"` // BalanceNotifyThreshold holds the value of the "balance_notify_threshold" field. BalanceNotifyThreshold *float64 `json:"balance_notify_threshold,omitempty"` // BalanceNotifyExtraEmails holds the value of the "balance_notify_extra_emails" field. BalanceNotifyExtraEmails string `json:"balance_notify_extra_emails,omitempty"` + // TotalRecharged holds the value of the "total_recharged" field. + TotalRecharged float64 `json:"total_recharged,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the UserQuery when eager-loading is set. Edges UserEdges `json:"edges"` @@ -192,11 +196,11 @@ func (*User) scanValues(columns []string) ([]any, error) { switch columns[i] { case user.FieldTotpEnabled, user.FieldBalanceNotifyEnabled: values[i] = new(sql.NullBool) - case user.FieldBalance, user.FieldBalanceNotifyThreshold: + case user.FieldBalance, user.FieldBalanceNotifyThreshold, user.FieldTotalRecharged: values[i] = new(sql.NullFloat64) case user.FieldID, user.FieldConcurrency: values[i] = new(sql.NullInt64) - case user.FieldEmail, user.FieldPasswordHash, user.FieldRole, user.FieldStatus, user.FieldUsername, user.FieldNotes, user.FieldTotpSecretEncrypted, user.FieldBalanceNotifyExtraEmails: + case user.FieldEmail, user.FieldPasswordHash, user.FieldRole, user.FieldStatus, user.FieldUsername, user.FieldNotes, user.FieldTotpSecretEncrypted, user.FieldBalanceNotifyThresholdType, user.FieldBalanceNotifyExtraEmails: values[i] = new(sql.NullString) case user.FieldCreatedAt, user.FieldUpdatedAt, user.FieldDeletedAt, user.FieldTotpEnabledAt: values[i] = new(sql.NullTime) @@ -314,6 +318,12 @@ func (_m *User) assignValues(columns []string, values []any) error { } else if value.Valid { _m.BalanceNotifyEnabled = value.Bool } + case user.FieldBalanceNotifyThresholdType: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field balance_notify_threshold_type", values[i]) + } else if value.Valid { + _m.BalanceNotifyThresholdType = value.String + } case user.FieldBalanceNotifyThreshold: if value, ok := values[i].(*sql.NullFloat64); !ok { return fmt.Errorf("unexpected type %T for field balance_notify_threshold", values[i]) @@ -327,6 +337,12 @@ func (_m *User) assignValues(columns []string, values []any) error { } else if value.Valid { _m.BalanceNotifyExtraEmails = value.String } + case user.FieldTotalRecharged: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field total_recharged", values[i]) + } else if value.Valid { + _m.TotalRecharged = value.Float64 + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -469,6 +485,9 @@ func (_m *User) String() string { builder.WriteString("balance_notify_enabled=") builder.WriteString(fmt.Sprintf("%v", _m.BalanceNotifyEnabled)) builder.WriteString(", ") + builder.WriteString("balance_notify_threshold_type=") + builder.WriteString(_m.BalanceNotifyThresholdType) + builder.WriteString(", ") if v := _m.BalanceNotifyThreshold; v != nil { builder.WriteString("balance_notify_threshold=") builder.WriteString(fmt.Sprintf("%v", *v)) @@ -476,6 +495,9 @@ func (_m *User) String() string { builder.WriteString(", ") builder.WriteString("balance_notify_extra_emails=") builder.WriteString(_m.BalanceNotifyExtraEmails) + builder.WriteString(", ") + builder.WriteString("total_recharged=") + builder.WriteString(fmt.Sprintf("%v", _m.TotalRecharged)) builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/user/user.go b/backend/ent/user/user.go index aff37013..d88a3a38 100644 --- a/backend/ent/user/user.go +++ b/backend/ent/user/user.go @@ -45,10 +45,14 @@ const ( FieldTotpEnabledAt = "totp_enabled_at" // FieldBalanceNotifyEnabled holds the string denoting the balance_notify_enabled field in the database. FieldBalanceNotifyEnabled = "balance_notify_enabled" + // FieldBalanceNotifyThresholdType holds the string denoting the balance_notify_threshold_type field in the database. + FieldBalanceNotifyThresholdType = "balance_notify_threshold_type" // FieldBalanceNotifyThreshold holds the string denoting the balance_notify_threshold field in the database. FieldBalanceNotifyThreshold = "balance_notify_threshold" // FieldBalanceNotifyExtraEmails holds the string denoting the balance_notify_extra_emails field in the database. FieldBalanceNotifyExtraEmails = "balance_notify_extra_emails" + // FieldTotalRecharged holds the string denoting the total_recharged field in the database. + FieldTotalRecharged = "total_recharged" // EdgeAPIKeys holds the string denoting the api_keys edge name in mutations. EdgeAPIKeys = "api_keys" // EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations. @@ -168,8 +172,10 @@ var Columns = []string{ FieldTotpEnabled, FieldTotpEnabledAt, FieldBalanceNotifyEnabled, + FieldBalanceNotifyThresholdType, FieldBalanceNotifyThreshold, FieldBalanceNotifyExtraEmails, + FieldTotalRecharged, } var ( @@ -228,8 +234,12 @@ var ( DefaultTotpEnabled bool // DefaultBalanceNotifyEnabled holds the default value on creation for the "balance_notify_enabled" field. DefaultBalanceNotifyEnabled bool + // DefaultBalanceNotifyThresholdType holds the default value on creation for the "balance_notify_threshold_type" field. + DefaultBalanceNotifyThresholdType string // DefaultBalanceNotifyExtraEmails holds the default value on creation for the "balance_notify_extra_emails" field. DefaultBalanceNotifyExtraEmails string + // DefaultTotalRecharged holds the default value on creation for the "total_recharged" field. + DefaultTotalRecharged float64 ) // OrderOption defines the ordering options for the User queries. @@ -315,6 +325,11 @@ func ByBalanceNotifyEnabled(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldBalanceNotifyEnabled, opts...).ToFunc() } +// ByBalanceNotifyThresholdType orders the results by the balance_notify_threshold_type field. +func ByBalanceNotifyThresholdType(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBalanceNotifyThresholdType, opts...).ToFunc() +} + // ByBalanceNotifyThreshold orders the results by the balance_notify_threshold field. func ByBalanceNotifyThreshold(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldBalanceNotifyThreshold, opts...).ToFunc() @@ -325,6 +340,11 @@ func ByBalanceNotifyExtraEmails(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldBalanceNotifyExtraEmails, opts...).ToFunc() } +// ByTotalRecharged orders the results by the total_recharged field. +func ByTotalRecharged(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldTotalRecharged, opts...).ToFunc() +} + // ByAPIKeysCount orders the results by api_keys count. func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/backend/ent/user/where.go b/backend/ent/user/where.go index 11a0318f..2788aa7a 100644 --- a/backend/ent/user/where.go +++ b/backend/ent/user/where.go @@ -130,6 +130,11 @@ func BalanceNotifyEnabled(v bool) predicate.User { return predicate.User(sql.FieldEQ(FieldBalanceNotifyEnabled, v)) } +// BalanceNotifyThresholdType applies equality check predicate on the "balance_notify_threshold_type" field. It's identical to BalanceNotifyThresholdTypeEQ. +func BalanceNotifyThresholdType(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldBalanceNotifyThresholdType, v)) +} + // BalanceNotifyThreshold applies equality check predicate on the "balance_notify_threshold" field. It's identical to BalanceNotifyThresholdEQ. func BalanceNotifyThreshold(v float64) predicate.User { return predicate.User(sql.FieldEQ(FieldBalanceNotifyThreshold, v)) @@ -140,6 +145,11 @@ func BalanceNotifyExtraEmails(v string) predicate.User { return predicate.User(sql.FieldEQ(FieldBalanceNotifyExtraEmails, v)) } +// TotalRecharged applies equality check predicate on the "total_recharged" field. It's identical to TotalRechargedEQ. +func TotalRecharged(v float64) predicate.User { + return predicate.User(sql.FieldEQ(FieldTotalRecharged, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.User { return predicate.User(sql.FieldEQ(FieldCreatedAt, v)) @@ -885,6 +895,71 @@ func BalanceNotifyEnabledNEQ(v bool) predicate.User { return predicate.User(sql.FieldNEQ(FieldBalanceNotifyEnabled, v)) } +// BalanceNotifyThresholdTypeEQ applies the EQ predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeEQ(v string) predicate.User { + return predicate.User(sql.FieldEQ(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeNEQ applies the NEQ predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeNEQ(v string) predicate.User { + return predicate.User(sql.FieldNEQ(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeIn applies the In predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeIn(vs ...string) predicate.User { + return predicate.User(sql.FieldIn(FieldBalanceNotifyThresholdType, vs...)) +} + +// BalanceNotifyThresholdTypeNotIn applies the NotIn predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeNotIn(vs ...string) predicate.User { + return predicate.User(sql.FieldNotIn(FieldBalanceNotifyThresholdType, vs...)) +} + +// BalanceNotifyThresholdTypeGT applies the GT predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeGT(v string) predicate.User { + return predicate.User(sql.FieldGT(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeGTE applies the GTE predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeGTE(v string) predicate.User { + return predicate.User(sql.FieldGTE(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeLT applies the LT predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeLT(v string) predicate.User { + return predicate.User(sql.FieldLT(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeLTE applies the LTE predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeLTE(v string) predicate.User { + return predicate.User(sql.FieldLTE(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeContains applies the Contains predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeContains(v string) predicate.User { + return predicate.User(sql.FieldContains(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeHasPrefix applies the HasPrefix predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeHasPrefix(v string) predicate.User { + return predicate.User(sql.FieldHasPrefix(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeHasSuffix applies the HasSuffix predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeHasSuffix(v string) predicate.User { + return predicate.User(sql.FieldHasSuffix(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeEqualFold applies the EqualFold predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeEqualFold(v string) predicate.User { + return predicate.User(sql.FieldEqualFold(FieldBalanceNotifyThresholdType, v)) +} + +// BalanceNotifyThresholdTypeContainsFold applies the ContainsFold predicate on the "balance_notify_threshold_type" field. +func BalanceNotifyThresholdTypeContainsFold(v string) predicate.User { + return predicate.User(sql.FieldContainsFold(FieldBalanceNotifyThresholdType, v)) +} + // BalanceNotifyThresholdEQ applies the EQ predicate on the "balance_notify_threshold" field. func BalanceNotifyThresholdEQ(v float64) predicate.User { return predicate.User(sql.FieldEQ(FieldBalanceNotifyThreshold, v)) @@ -1000,6 +1075,46 @@ func BalanceNotifyExtraEmailsContainsFold(v string) predicate.User { return predicate.User(sql.FieldContainsFold(FieldBalanceNotifyExtraEmails, v)) } +// TotalRechargedEQ applies the EQ predicate on the "total_recharged" field. +func TotalRechargedEQ(v float64) predicate.User { + return predicate.User(sql.FieldEQ(FieldTotalRecharged, v)) +} + +// TotalRechargedNEQ applies the NEQ predicate on the "total_recharged" field. +func TotalRechargedNEQ(v float64) predicate.User { + return predicate.User(sql.FieldNEQ(FieldTotalRecharged, v)) +} + +// TotalRechargedIn applies the In predicate on the "total_recharged" field. +func TotalRechargedIn(vs ...float64) predicate.User { + return predicate.User(sql.FieldIn(FieldTotalRecharged, vs...)) +} + +// TotalRechargedNotIn applies the NotIn predicate on the "total_recharged" field. +func TotalRechargedNotIn(vs ...float64) predicate.User { + return predicate.User(sql.FieldNotIn(FieldTotalRecharged, vs...)) +} + +// TotalRechargedGT applies the GT predicate on the "total_recharged" field. +func TotalRechargedGT(v float64) predicate.User { + return predicate.User(sql.FieldGT(FieldTotalRecharged, v)) +} + +// TotalRechargedGTE applies the GTE predicate on the "total_recharged" field. +func TotalRechargedGTE(v float64) predicate.User { + return predicate.User(sql.FieldGTE(FieldTotalRecharged, v)) +} + +// TotalRechargedLT applies the LT predicate on the "total_recharged" field. +func TotalRechargedLT(v float64) predicate.User { + return predicate.User(sql.FieldLT(FieldTotalRecharged, v)) +} + +// TotalRechargedLTE applies the LTE predicate on the "total_recharged" field. +func TotalRechargedLTE(v float64) predicate.User { + return predicate.User(sql.FieldLTE(FieldTotalRecharged, v)) +} + // HasAPIKeys applies the HasEdge predicate on the "api_keys" edge. func HasAPIKeys() predicate.User { return predicate.User(func(s *sql.Selector) { diff --git a/backend/ent/user_create.go b/backend/ent/user_create.go index 955fde72..fbc64f9c 100644 --- a/backend/ent/user_create.go +++ b/backend/ent/user_create.go @@ -225,6 +225,20 @@ func (_c *UserCreate) SetNillableBalanceNotifyEnabled(v *bool) *UserCreate { return _c } +// SetBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field. +func (_c *UserCreate) SetBalanceNotifyThresholdType(v string) *UserCreate { + _c.mutation.SetBalanceNotifyThresholdType(v) + return _c +} + +// SetNillableBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field if the given value is not nil. +func (_c *UserCreate) SetNillableBalanceNotifyThresholdType(v *string) *UserCreate { + if v != nil { + _c.SetBalanceNotifyThresholdType(*v) + } + return _c +} + // SetBalanceNotifyThreshold sets the "balance_notify_threshold" field. func (_c *UserCreate) SetBalanceNotifyThreshold(v float64) *UserCreate { _c.mutation.SetBalanceNotifyThreshold(v) @@ -253,6 +267,20 @@ func (_c *UserCreate) SetNillableBalanceNotifyExtraEmails(v *string) *UserCreate return _c } +// SetTotalRecharged sets the "total_recharged" field. +func (_c *UserCreate) SetTotalRecharged(v float64) *UserCreate { + _c.mutation.SetTotalRecharged(v) + return _c +} + +// SetNillableTotalRecharged sets the "total_recharged" field if the given value is not nil. +func (_c *UserCreate) SetNillableTotalRecharged(v *float64) *UserCreate { + if v != nil { + _c.SetTotalRecharged(*v) + } + return _c +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_c *UserCreate) AddAPIKeyIDs(ids ...int64) *UserCreate { _c.mutation.AddAPIKeyIDs(ids...) @@ -486,10 +514,18 @@ func (_c *UserCreate) defaults() error { v := user.DefaultBalanceNotifyEnabled _c.mutation.SetBalanceNotifyEnabled(v) } + if _, ok := _c.mutation.BalanceNotifyThresholdType(); !ok { + v := user.DefaultBalanceNotifyThresholdType + _c.mutation.SetBalanceNotifyThresholdType(v) + } if _, ok := _c.mutation.BalanceNotifyExtraEmails(); !ok { v := user.DefaultBalanceNotifyExtraEmails _c.mutation.SetBalanceNotifyExtraEmails(v) } + if _, ok := _c.mutation.TotalRecharged(); !ok { + v := user.DefaultTotalRecharged + _c.mutation.SetTotalRecharged(v) + } return nil } @@ -556,9 +592,15 @@ func (_c *UserCreate) check() error { if _, ok := _c.mutation.BalanceNotifyEnabled(); !ok { return &ValidationError{Name: "balance_notify_enabled", err: errors.New(`ent: missing required field "User.balance_notify_enabled"`)} } + if _, ok := _c.mutation.BalanceNotifyThresholdType(); !ok { + return &ValidationError{Name: "balance_notify_threshold_type", err: errors.New(`ent: missing required field "User.balance_notify_threshold_type"`)} + } if _, ok := _c.mutation.BalanceNotifyExtraEmails(); !ok { return &ValidationError{Name: "balance_notify_extra_emails", err: errors.New(`ent: missing required field "User.balance_notify_extra_emails"`)} } + if _, ok := _c.mutation.TotalRecharged(); !ok { + return &ValidationError{Name: "total_recharged", err: errors.New(`ent: missing required field "User.total_recharged"`)} + } return nil } @@ -646,6 +688,10 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { _spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value) _node.BalanceNotifyEnabled = value } + if value, ok := _c.mutation.BalanceNotifyThresholdType(); ok { + _spec.SetField(user.FieldBalanceNotifyThresholdType, field.TypeString, value) + _node.BalanceNotifyThresholdType = value + } if value, ok := _c.mutation.BalanceNotifyThreshold(); ok { _spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value) _node.BalanceNotifyThreshold = &value @@ -654,6 +700,10 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { _spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value) _node.BalanceNotifyExtraEmails = value } + if value, ok := _c.mutation.TotalRecharged(); ok { + _spec.SetField(user.FieldTotalRecharged, field.TypeFloat64, value) + _node.TotalRecharged = value + } if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1068,6 +1118,18 @@ func (u *UserUpsert) UpdateBalanceNotifyEnabled() *UserUpsert { return u } +// SetBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field. +func (u *UserUpsert) SetBalanceNotifyThresholdType(v string) *UserUpsert { + u.Set(user.FieldBalanceNotifyThresholdType, v) + return u +} + +// UpdateBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field to the value that was provided on create. +func (u *UserUpsert) UpdateBalanceNotifyThresholdType() *UserUpsert { + u.SetExcluded(user.FieldBalanceNotifyThresholdType) + return u +} + // SetBalanceNotifyThreshold sets the "balance_notify_threshold" field. func (u *UserUpsert) SetBalanceNotifyThreshold(v float64) *UserUpsert { u.Set(user.FieldBalanceNotifyThreshold, v) @@ -1104,6 +1166,24 @@ func (u *UserUpsert) UpdateBalanceNotifyExtraEmails() *UserUpsert { return u } +// SetTotalRecharged sets the "total_recharged" field. +func (u *UserUpsert) SetTotalRecharged(v float64) *UserUpsert { + u.Set(user.FieldTotalRecharged, v) + return u +} + +// UpdateTotalRecharged sets the "total_recharged" field to the value that was provided on create. +func (u *UserUpsert) UpdateTotalRecharged() *UserUpsert { + u.SetExcluded(user.FieldTotalRecharged) + return u +} + +// AddTotalRecharged adds v to the "total_recharged" field. +func (u *UserUpsert) AddTotalRecharged(v float64) *UserUpsert { + u.Add(user.FieldTotalRecharged, v) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -1380,6 +1460,20 @@ func (u *UserUpsertOne) UpdateBalanceNotifyEnabled() *UserUpsertOne { }) } +// SetBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field. +func (u *UserUpsertOne) SetBalanceNotifyThresholdType(v string) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetBalanceNotifyThresholdType(v) + }) +} + +// UpdateBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateBalanceNotifyThresholdType() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateBalanceNotifyThresholdType() + }) +} + // SetBalanceNotifyThreshold sets the "balance_notify_threshold" field. func (u *UserUpsertOne) SetBalanceNotifyThreshold(v float64) *UserUpsertOne { return u.Update(func(s *UserUpsert) { @@ -1422,6 +1516,27 @@ func (u *UserUpsertOne) UpdateBalanceNotifyExtraEmails() *UserUpsertOne { }) } +// SetTotalRecharged sets the "total_recharged" field. +func (u *UserUpsertOne) SetTotalRecharged(v float64) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.SetTotalRecharged(v) + }) +} + +// AddTotalRecharged adds v to the "total_recharged" field. +func (u *UserUpsertOne) AddTotalRecharged(v float64) *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.AddTotalRecharged(v) + }) +} + +// UpdateTotalRecharged sets the "total_recharged" field to the value that was provided on create. +func (u *UserUpsertOne) UpdateTotalRecharged() *UserUpsertOne { + return u.Update(func(s *UserUpsert) { + s.UpdateTotalRecharged() + }) +} + // Exec executes the query. func (u *UserUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -1864,6 +1979,20 @@ func (u *UserUpsertBulk) UpdateBalanceNotifyEnabled() *UserUpsertBulk { }) } +// SetBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field. +func (u *UserUpsertBulk) SetBalanceNotifyThresholdType(v string) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetBalanceNotifyThresholdType(v) + }) +} + +// UpdateBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateBalanceNotifyThresholdType() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateBalanceNotifyThresholdType() + }) +} + // SetBalanceNotifyThreshold sets the "balance_notify_threshold" field. func (u *UserUpsertBulk) SetBalanceNotifyThreshold(v float64) *UserUpsertBulk { return u.Update(func(s *UserUpsert) { @@ -1906,6 +2035,27 @@ func (u *UserUpsertBulk) UpdateBalanceNotifyExtraEmails() *UserUpsertBulk { }) } +// SetTotalRecharged sets the "total_recharged" field. +func (u *UserUpsertBulk) SetTotalRecharged(v float64) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.SetTotalRecharged(v) + }) +} + +// AddTotalRecharged adds v to the "total_recharged" field. +func (u *UserUpsertBulk) AddTotalRecharged(v float64) *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.AddTotalRecharged(v) + }) +} + +// UpdateTotalRecharged sets the "total_recharged" field to the value that was provided on create. +func (u *UserUpsertBulk) UpdateTotalRecharged() *UserUpsertBulk { + return u.Update(func(s *UserUpsert) { + s.UpdateTotalRecharged() + }) +} + // Exec executes the query. func (u *UserUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/backend/ent/user_update.go b/backend/ent/user_update.go index 823df0b6..6b355247 100644 --- a/backend/ent/user_update.go +++ b/backend/ent/user_update.go @@ -257,6 +257,20 @@ func (_u *UserUpdate) SetNillableBalanceNotifyEnabled(v *bool) *UserUpdate { return _u } +// SetBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field. +func (_u *UserUpdate) SetBalanceNotifyThresholdType(v string) *UserUpdate { + _u.mutation.SetBalanceNotifyThresholdType(v) + return _u +} + +// SetNillableBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field if the given value is not nil. +func (_u *UserUpdate) SetNillableBalanceNotifyThresholdType(v *string) *UserUpdate { + if v != nil { + _u.SetBalanceNotifyThresholdType(*v) + } + return _u +} + // SetBalanceNotifyThreshold sets the "balance_notify_threshold" field. func (_u *UserUpdate) SetBalanceNotifyThreshold(v float64) *UserUpdate { _u.mutation.ResetBalanceNotifyThreshold() @@ -298,6 +312,27 @@ func (_u *UserUpdate) SetNillableBalanceNotifyExtraEmails(v *string) *UserUpdate return _u } +// SetTotalRecharged sets the "total_recharged" field. +func (_u *UserUpdate) SetTotalRecharged(v float64) *UserUpdate { + _u.mutation.ResetTotalRecharged() + _u.mutation.SetTotalRecharged(v) + return _u +} + +// SetNillableTotalRecharged sets the "total_recharged" field if the given value is not nil. +func (_u *UserUpdate) SetNillableTotalRecharged(v *float64) *UserUpdate { + if v != nil { + _u.SetTotalRecharged(*v) + } + return _u +} + +// AddTotalRecharged adds value to the "total_recharged" field. +func (_u *UserUpdate) AddTotalRecharged(v float64) *UserUpdate { + _u.mutation.AddTotalRecharged(v) + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *UserUpdate) AddAPIKeyIDs(ids ...int64) *UserUpdate { _u.mutation.AddAPIKeyIDs(ids...) @@ -804,6 +839,9 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.BalanceNotifyEnabled(); ok { _spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value) } + if value, ok := _u.mutation.BalanceNotifyThresholdType(); ok { + _spec.SetField(user.FieldBalanceNotifyThresholdType, field.TypeString, value) + } if value, ok := _u.mutation.BalanceNotifyThreshold(); ok { _spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value) } @@ -816,6 +854,12 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.BalanceNotifyExtraEmails(); ok { _spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value) } + if value, ok := _u.mutation.TotalRecharged(); ok { + _spec.SetField(user.FieldTotalRecharged, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedTotalRecharged(); ok { + _spec.AddField(user.FieldTotalRecharged, field.TypeFloat64, value) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1518,6 +1562,20 @@ func (_u *UserUpdateOne) SetNillableBalanceNotifyEnabled(v *bool) *UserUpdateOne return _u } +// SetBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field. +func (_u *UserUpdateOne) SetBalanceNotifyThresholdType(v string) *UserUpdateOne { + _u.mutation.SetBalanceNotifyThresholdType(v) + return _u +} + +// SetNillableBalanceNotifyThresholdType sets the "balance_notify_threshold_type" field if the given value is not nil. +func (_u *UserUpdateOne) SetNillableBalanceNotifyThresholdType(v *string) *UserUpdateOne { + if v != nil { + _u.SetBalanceNotifyThresholdType(*v) + } + return _u +} + // SetBalanceNotifyThreshold sets the "balance_notify_threshold" field. func (_u *UserUpdateOne) SetBalanceNotifyThreshold(v float64) *UserUpdateOne { _u.mutation.ResetBalanceNotifyThreshold() @@ -1559,6 +1617,27 @@ func (_u *UserUpdateOne) SetNillableBalanceNotifyExtraEmails(v *string) *UserUpd return _u } +// SetTotalRecharged sets the "total_recharged" field. +func (_u *UserUpdateOne) SetTotalRecharged(v float64) *UserUpdateOne { + _u.mutation.ResetTotalRecharged() + _u.mutation.SetTotalRecharged(v) + return _u +} + +// SetNillableTotalRecharged sets the "total_recharged" field if the given value is not nil. +func (_u *UserUpdateOne) SetNillableTotalRecharged(v *float64) *UserUpdateOne { + if v != nil { + _u.SetTotalRecharged(*v) + } + return _u +} + +// AddTotalRecharged adds value to the "total_recharged" field. +func (_u *UserUpdateOne) AddTotalRecharged(v float64) *UserUpdateOne { + _u.mutation.AddTotalRecharged(v) + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *UserUpdateOne) AddAPIKeyIDs(ids ...int64) *UserUpdateOne { _u.mutation.AddAPIKeyIDs(ids...) @@ -2095,6 +2174,9 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) { if value, ok := _u.mutation.BalanceNotifyEnabled(); ok { _spec.SetField(user.FieldBalanceNotifyEnabled, field.TypeBool, value) } + if value, ok := _u.mutation.BalanceNotifyThresholdType(); ok { + _spec.SetField(user.FieldBalanceNotifyThresholdType, field.TypeString, value) + } if value, ok := _u.mutation.BalanceNotifyThreshold(); ok { _spec.SetField(user.FieldBalanceNotifyThreshold, field.TypeFloat64, value) } @@ -2107,6 +2189,12 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) { if value, ok := _u.mutation.BalanceNotifyExtraEmails(); ok { _spec.SetField(user.FieldBalanceNotifyExtraEmails, field.TypeString, value) } + if value, ok := _u.mutation.TotalRecharged(); ok { + _spec.SetField(user.FieldTotalRecharged, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedTotalRecharged(); ok { + _spec.AddField(user.FieldTotalRecharged, field.TypeFloat64, value) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/backend/internal/handler/admin/setting_handler.go b/backend/internal/handler/admin/setting_handler.go index e5e024c6..3d587a21 100644 --- a/backend/internal/handler/admin/setting_handler.go +++ b/backend/internal/handler/admin/setting_handler.go @@ -176,6 +176,10 @@ func (h *SettingHandler) GetSettings(c *gin.Context) { EnableMetadataPassthrough: settings.EnableMetadataPassthrough, EnableCCHSigning: settings.EnableCCHSigning, WebSearchEmulationEnabled: settings.WebSearchEmulationEnabled, + BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled, + BalanceLowNotifyThresholdType: settings.BalanceLowNotifyThresholdType, + BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold, + AccountQuotaNotifyEmails: settings.AccountQuotaNotifyEmails, PaymentEnabled: paymentCfg.Enabled, PaymentMinAmount: paymentCfg.MinAmount, PaymentMaxAmount: paymentCfg.MaxAmount, @@ -305,6 +309,12 @@ type UpdateSettingsRequest struct { EnableMetadataPassthrough *bool `json:"enable_metadata_passthrough"` EnableCCHSigning *bool `json:"enable_cch_signing"` + // Balance low notification + BalanceLowNotifyEnabled *bool `json:"balance_low_notify_enabled"` + BalanceLowNotifyThresholdType *string `json:"balance_low_notify_threshold_type"` + BalanceLowNotifyThreshold *float64 `json:"balance_low_notify_threshold"` + AccountQuotaNotifyEmails *[]string `json:"account_quota_notify_emails"` + // Payment configuration (integrated into settings, full replace) PaymentEnabled *bool `json:"payment_enabled"` PaymentMinAmount *float64 `json:"payment_min_amount"` @@ -882,6 +892,30 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { } return previousSettings.EnableCCHSigning }(), + BalanceLowNotifyEnabled: func() bool { + if req.BalanceLowNotifyEnabled != nil { + return *req.BalanceLowNotifyEnabled + } + return previousSettings.BalanceLowNotifyEnabled + }(), + BalanceLowNotifyThresholdType: func() string { + if req.BalanceLowNotifyThresholdType != nil { + return *req.BalanceLowNotifyThresholdType + } + return previousSettings.BalanceLowNotifyThresholdType + }(), + BalanceLowNotifyThreshold: func() float64 { + if req.BalanceLowNotifyThreshold != nil { + return *req.BalanceLowNotifyThreshold + } + return previousSettings.BalanceLowNotifyThreshold + }(), + AccountQuotaNotifyEmails: func() []string { + if req.AccountQuotaNotifyEmails != nil { + return *req.AccountQuotaNotifyEmails + } + return previousSettings.AccountQuotaNotifyEmails + }(), } if err := h.settingService.UpdateSettings(c.Request.Context(), settings); err != nil { @@ -1028,6 +1062,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) { EnableFingerprintUnification: updatedSettings.EnableFingerprintUnification, EnableMetadataPassthrough: updatedSettings.EnableMetadataPassthrough, EnableCCHSigning: updatedSettings.EnableCCHSigning, + BalanceLowNotifyEnabled: updatedSettings.BalanceLowNotifyEnabled, + BalanceLowNotifyThresholdType: updatedSettings.BalanceLowNotifyThresholdType, + BalanceLowNotifyThreshold: updatedSettings.BalanceLowNotifyThreshold, + AccountQuotaNotifyEmails: updatedSettings.AccountQuotaNotifyEmails, PaymentEnabled: updatedPaymentCfg.Enabled, PaymentMinAmount: updatedPaymentCfg.MinAmount, PaymentMaxAmount: updatedPaymentCfg.MaxAmount, diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index a465c7fb..147072c3 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -13,19 +13,21 @@ func UserFromServiceShallow(u *service.User) *User { return nil } return &User{ - ID: u.ID, - Email: u.Email, - Username: u.Username, - Role: u.Role, - Balance: u.Balance, - Concurrency: u.Concurrency, - Status: u.Status, - AllowedGroups: u.AllowedGroups, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, - BalanceNotifyEnabled: u.BalanceNotifyEnabled, - BalanceNotifyThreshold: u.BalanceNotifyThreshold, - BalanceNotifyExtraEmails: u.BalanceNotifyExtraEmails, + ID: u.ID, + Email: u.Email, + Username: u.Username, + Role: u.Role, + Balance: u.Balance, + Concurrency: u.Concurrency, + Status: u.Status, + AllowedGroups: u.AllowedGroups, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + BalanceNotifyEnabled: u.BalanceNotifyEnabled, + BalanceNotifyThresholdType: u.BalanceNotifyThresholdType, + BalanceNotifyThreshold: u.BalanceNotifyThreshold, + BalanceNotifyExtraEmails: u.BalanceNotifyExtraEmails, + TotalRecharged: u.TotalRecharged, } } diff --git a/backend/internal/handler/dto/settings.go b/backend/internal/handler/dto/settings.go index e29f72da..8da7c6f2 100644 --- a/backend/internal/handler/dto/settings.go +++ b/backend/internal/handler/dto/settings.go @@ -150,9 +150,10 @@ type SystemSettings struct { PaymentCancelRateLimitMode string `json:"payment_cancel_rate_limit_window_mode"` // Balance low notification - BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"` - BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"` - AccountQuotaNotifyEmails []string `json:"account_quota_notify_emails"` + BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"` + BalanceLowNotifyThresholdType string `json:"balance_low_notify_threshold_type"` + BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"` + AccountQuotaNotifyEmails []string `json:"account_quota_notify_emails"` } type DefaultSubscriptionSetting struct { diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 18522868..425d3df9 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -19,9 +19,11 @@ type User struct { UpdatedAt time.Time `json:"updated_at"` // 余额不足通知 - BalanceNotifyEnabled bool `json:"balance_notify_enabled"` - BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"` - BalanceNotifyExtraEmails []string `json:"balance_notify_extra_emails"` + BalanceNotifyEnabled bool `json:"balance_notify_enabled"` + BalanceNotifyThresholdType string `json:"balance_notify_threshold_type"` + BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"` + BalanceNotifyExtraEmails []string `json:"balance_notify_extra_emails"` + TotalRecharged float64 `json:"total_recharged"` APIKeys []APIKey `json:"api_keys,omitempty"` Subscriptions []UserSubscription `json:"subscriptions,omitempty"` diff --git a/backend/internal/handler/user_handler.go b/backend/internal/handler/user_handler.go index 4fb72ce7..48528d55 100644 --- a/backend/internal/handler/user_handler.go +++ b/backend/internal/handler/user_handler.go @@ -33,9 +33,10 @@ type ChangePasswordRequest struct { // UpdateProfileRequest represents the update profile request payload type UpdateProfileRequest struct { - Username *string `json:"username"` - BalanceNotifyEnabled *bool `json:"balance_notify_enabled"` - BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"` + Username *string `json:"username"` + BalanceNotifyEnabled *bool `json:"balance_notify_enabled"` + BalanceNotifyThresholdType *string `json:"balance_notify_threshold_type"` + BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"` } // GetProfile handles getting user profile @@ -100,9 +101,10 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) { } svcReq := service.UpdateProfileRequest{ - Username: req.Username, - BalanceNotifyEnabled: req.BalanceNotifyEnabled, - BalanceNotifyThreshold: req.BalanceNotifyThreshold, + Username: req.Username, + BalanceNotifyEnabled: req.BalanceNotifyEnabled, + BalanceNotifyThresholdType: req.BalanceNotifyThresholdType, + BalanceNotifyThreshold: req.BalanceNotifyThreshold, } updatedUser, err := h.userService.UpdateProfile(c.Request.Context(), subject.UserID, svcReq) if err != nil { diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index 752a5937..4ecab47a 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -641,22 +641,24 @@ func userEntityToService(u *dbent.User) *service.User { return nil } out := &service.User{ - ID: u.ID, - Email: u.Email, - Username: u.Username, - Notes: u.Notes, - PasswordHash: u.PasswordHash, - Role: u.Role, - Balance: u.Balance, - Concurrency: u.Concurrency, - Status: u.Status, - TotpSecretEncrypted: u.TotpSecretEncrypted, - TotpEnabled: u.TotpEnabled, - TotpEnabledAt: u.TotpEnabledAt, - BalanceNotifyEnabled: u.BalanceNotifyEnabled, - BalanceNotifyThreshold: u.BalanceNotifyThreshold, - CreatedAt: u.CreatedAt, - UpdatedAt: u.UpdatedAt, + ID: u.ID, + Email: u.Email, + Username: u.Username, + Notes: u.Notes, + PasswordHash: u.PasswordHash, + Role: u.Role, + Balance: u.Balance, + Concurrency: u.Concurrency, + Status: u.Status, + TotpSecretEncrypted: u.TotpSecretEncrypted, + TotpEnabled: u.TotpEnabled, + TotpEnabledAt: u.TotpEnabledAt, + BalanceNotifyEnabled: u.BalanceNotifyEnabled, + BalanceNotifyThresholdType: u.BalanceNotifyThresholdType, + BalanceNotifyThreshold: u.BalanceNotifyThreshold, + TotalRecharged: u.TotalRecharged, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, } // Parse extra emails JSON array if u.BalanceNotifyExtraEmails != "" && u.BalanceNotifyExtraEmails != "[]" { diff --git a/backend/internal/repository/user_repo.go b/backend/internal/repository/user_repo.go index 2c544857..63168fb1 100644 --- a/backend/internal/repository/user_repo.go +++ b/backend/internal/repository/user_repo.go @@ -148,6 +148,7 @@ func (r *userRepository) Update(ctx context.Context, userIn *service.User) error SetConcurrency(userIn.Concurrency). SetStatus(userIn.Status). SetBalanceNotifyEnabled(userIn.BalanceNotifyEnabled). + SetBalanceNotifyThresholdType(userIn.BalanceNotifyThresholdType). SetNillableBalanceNotifyThreshold(userIn.BalanceNotifyThreshold). SetBalanceNotifyExtraEmails(marshalExtraEmails(userIn.BalanceNotifyExtraEmails)) if userIn.BalanceNotifyThreshold == nil { @@ -389,7 +390,12 @@ func (r *userRepository) filterUsersByAttributes(ctx context.Context, attrs map[ func (r *userRepository) UpdateBalance(ctx context.Context, id int64, amount float64) error { client := clientFromContext(ctx, r.client) - n, err := client.User.Update().Where(dbuser.IDEQ(id)).AddBalance(amount).Save(ctx) + update := client.User.Update().Where(dbuser.IDEQ(id)).AddBalance(amount) + // Track cumulative recharge amount for percentage-based notifications + if amount > 0 { + update = update.AddTotalRecharged(amount) + } + n, err := update.Save(ctx) if err != nil { return translatePersistenceError(err, service.ErrUserNotFound, nil) } diff --git a/backend/internal/service/balance_notify_service.go b/backend/internal/service/balance_notify_service.go index 8dd56b8f..7fbdd254 100644 --- a/backend/internal/service/balance_notify_service.go +++ b/backend/internal/service/balance_notify_service.go @@ -47,30 +47,21 @@ func (s *BalanceNotifyService) CheckBalanceAfterDeduction(ctx context.Context, u if user == nil || s.emailService == nil || s.settingRepo == nil { return } - - // Check user-level switch if !user.BalanceNotifyEnabled { return } - // Check global switch - globalEnabled, threshold := s.getBalanceNotifyConfig(ctx) + globalEnabled, globalThresholdType, globalThresholdValue := s.getBalanceNotifyConfig(ctx) if !globalEnabled { return } - // User custom threshold overrides system default - if user.BalanceNotifyThreshold != nil { - threshold = *user.BalanceNotifyThreshold - } - + threshold := s.resolveEffectiveThreshold(user, globalThresholdType, globalThresholdValue) if threshold <= 0 { return } newBalance := oldBalance - cost - - // Only notify on first crossing if oldBalance >= threshold && newBalance < threshold { siteName := s.getSiteName(ctx) recipients := s.collectBalanceNotifyRecipients(user) @@ -85,6 +76,30 @@ func (s *BalanceNotifyService) CheckBalanceAfterDeduction(ctx context.Context, u } } +// resolveEffectiveThreshold computes the actual USD threshold based on type and user settings. +func (s *BalanceNotifyService) resolveEffectiveThreshold(user *User, globalType string, globalValue float64) float64 { + // User-level override takes full precedence + if user.BalanceNotifyThreshold != nil { + thresholdType := user.BalanceNotifyThresholdType + if thresholdType == "" { + thresholdType = globalType + } + return computeThreshold(thresholdType, *user.BalanceNotifyThreshold, user.TotalRecharged) + } + return computeThreshold(globalType, globalValue, user.TotalRecharged) +} + +// computeThreshold converts a threshold value to USD based on type. +func computeThreshold(thresholdType string, value, totalRecharged float64) float64 { + if thresholdType == ThresholdTypePercentage { + if totalRecharged <= 0 { + return 0 // no recharge history → skip percentage check + } + return totalRecharged * value / 100 + } + return value // fixed USD amount +} + // quotaDim describes one quota dimension for notification checking. type quotaDim struct { name string @@ -139,13 +154,21 @@ func (s *BalanceNotifyService) asyncSendQuotaAlert(adminEmails []string, account } // getBalanceNotifyConfig reads global balance notification settings. -func (s *BalanceNotifyService) getBalanceNotifyConfig(ctx context.Context) (enabled bool, threshold float64) { - keys := []string{SettingKeyBalanceLowNotifyEnabled, SettingKeyBalanceLowNotifyThreshold} +func (s *BalanceNotifyService) getBalanceNotifyConfig(ctx context.Context) (enabled bool, thresholdType string, threshold float64) { + keys := []string{ + SettingKeyBalanceLowNotifyEnabled, + SettingKeyBalanceLowNotifyThresholdType, + SettingKeyBalanceLowNotifyThreshold, + } settings, err := s.settingRepo.GetMultiple(ctx, keys) if err != nil { - return false, 0 + return false, ThresholdTypeFixed, 0 } enabled = settings[SettingKeyBalanceLowNotifyEnabled] == "true" + thresholdType = settings[SettingKeyBalanceLowNotifyThresholdType] + if thresholdType == "" { + thresholdType = ThresholdTypeFixed + } if v := settings[SettingKeyBalanceLowNotifyThreshold]; v != "" { if f, err := strconv.ParseFloat(v, 64); err == nil { threshold = f diff --git a/backend/internal/service/domain_constants.go b/backend/internal/service/domain_constants.go index 2704e0d0..3de0e343 100644 --- a/backend/internal/service/domain_constants.go +++ b/backend/internal/service/domain_constants.go @@ -251,8 +251,13 @@ const ( SettingKeyEnableCCHSigning = "enable_cch_signing" // Balance Low Notification - SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关 - SettingKeyBalanceLowNotifyThreshold = "balance_low_notify_threshold" // 默认阈值(USD) + SettingKeyBalanceLowNotifyEnabled = "balance_low_notify_enabled" // 全局开关 + SettingKeyBalanceLowNotifyThresholdType = "balance_low_notify_threshold_type" // "fixed" | "percentage" + SettingKeyBalanceLowNotifyThreshold = "balance_low_notify_threshold" // 默认阈值(USD 或百分比) + + // Threshold type constants + ThresholdTypeFixed = "fixed" + ThresholdTypePercentage = "percentage" // Account Quota Notification SettingKeyAccountQuotaNotifyEmails = "account_quota_notify_emails" // 管理员通知邮箱列表(JSON 数组) diff --git a/backend/internal/service/setting_service.go b/backend/internal/service/setting_service.go index bc4f53ce..e2491cbc 100644 --- a/backend/internal/service/setting_service.go +++ b/backend/internal/service/setting_service.go @@ -597,6 +597,11 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet // Balance low notification updates[SettingKeyBalanceLowNotifyEnabled] = strconv.FormatBool(settings.BalanceLowNotifyEnabled) + thresholdType := settings.BalanceLowNotifyThresholdType + if thresholdType == "" { + thresholdType = ThresholdTypeFixed + } + updates[SettingKeyBalanceLowNotifyThresholdType] = thresholdType updates[SettingKeyBalanceLowNotifyThreshold] = strconv.FormatFloat(settings.BalanceLowNotifyThreshold, 'f', 8, 64) accountQuotaNotifyEmailsJSON, err := json.Marshal(settings.AccountQuotaNotifyEmails) if err != nil { @@ -1228,6 +1233,10 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin // Balance low notification result.BalanceLowNotifyEnabled = settings[SettingKeyBalanceLowNotifyEnabled] == "true" + result.BalanceLowNotifyThresholdType = settings[SettingKeyBalanceLowNotifyThresholdType] + if result.BalanceLowNotifyThresholdType == "" { + result.BalanceLowNotifyThresholdType = ThresholdTypeFixed + } if v, err := strconv.ParseFloat(settings[SettingKeyBalanceLowNotifyThreshold], 64); err == nil && v >= 0 { result.BalanceLowNotifyThreshold = v } diff --git a/backend/internal/service/settings_view.go b/backend/internal/service/settings_view.go index debc2b19..b28d2247 100644 --- a/backend/internal/service/settings_view.go +++ b/backend/internal/service/settings_view.go @@ -108,8 +108,9 @@ type SystemSettings struct { EnableCCHSigning bool // 是否对 billing header cch 进行签名(默认 false) // Balance low notification - BalanceLowNotifyEnabled bool - BalanceLowNotifyThreshold float64 + BalanceLowNotifyEnabled bool + BalanceLowNotifyThresholdType string // "fixed" (default) | "percentage" + BalanceLowNotifyThreshold float64 // Account quota notification AccountQuotaNotifyEmails []string diff --git a/backend/internal/service/user.go b/backend/internal/service/user.go index b4818223..4ca31adc 100644 --- a/backend/internal/service/user.go +++ b/backend/internal/service/user.go @@ -31,9 +31,11 @@ type User struct { TotpEnabledAt *time.Time // TOTP 启用时间 // 余额不足通知 - BalanceNotifyEnabled bool - BalanceNotifyThreshold *float64 - BalanceNotifyExtraEmails []string + BalanceNotifyEnabled bool + BalanceNotifyThresholdType string // "fixed" (default) | "percentage" + BalanceNotifyThreshold *float64 + BalanceNotifyExtraEmails []string + TotalRecharged float64 APIKeys []APIKey Subscriptions []UserSubscription diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index e6b9a210..4669cb2b 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -62,11 +62,12 @@ type UserRepository interface { // UpdateProfileRequest 更新用户资料请求 type UpdateProfileRequest struct { - Email *string `json:"email"` - Username *string `json:"username"` - Concurrency *int `json:"concurrency"` - BalanceNotifyEnabled *bool `json:"balance_notify_enabled"` - BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"` + Email *string `json:"email"` + Username *string `json:"username"` + Concurrency *int `json:"concurrency"` + BalanceNotifyEnabled *bool `json:"balance_notify_enabled"` + BalanceNotifyThresholdType *string `json:"balance_notify_threshold_type"` + BalanceNotifyThreshold *float64 `json:"balance_notify_threshold"` } // ChangePasswordRequest 修改密码请求 @@ -143,6 +144,9 @@ func (s *UserService) UpdateProfile(ctx context.Context, userID int64, req Updat if req.BalanceNotifyEnabled != nil { user.BalanceNotifyEnabled = *req.BalanceNotifyEnabled } + if req.BalanceNotifyThresholdType != nil { + user.BalanceNotifyThresholdType = *req.BalanceNotifyThresholdType + } if req.BalanceNotifyThreshold != nil { if *req.BalanceNotifyThreshold <= 0 { user.BalanceNotifyThreshold = nil // clear to system default diff --git a/backend/migrations/102_add_balance_notify_threshold_type.sql b/backend/migrations/102_add_balance_notify_threshold_type.sql new file mode 100644 index 00000000..7ad70552 --- /dev/null +++ b/backend/migrations/102_add_balance_notify_threshold_type.sql @@ -0,0 +1,4 @@ +-- Add threshold type support (fixed / percentage) to balance notification +ALTER TABLE users ADD COLUMN IF NOT EXISTS balance_notify_threshold_type VARCHAR(10) NOT NULL DEFAULT 'fixed'; +-- Track cumulative recharge amount for percentage threshold calculation +ALTER TABLE users ADD COLUMN IF NOT EXISTS total_recharged DECIMAL(20,8) NOT NULL DEFAULT 0; diff --git a/frontend/src/api/admin/settings.ts b/frontend/src/api/admin/settings.ts index 31284289..ec290be5 100644 --- a/frontend/src/api/admin/settings.ts +++ b/frontend/src/api/admin/settings.ts @@ -137,6 +137,7 @@ export interface SystemSettings { // Balance & quota notification balance_low_notify_enabled: boolean + balance_low_notify_threshold_type: 'fixed' | 'percentage' balance_low_notify_threshold: number account_quota_notify_emails: string[] } @@ -240,6 +241,7 @@ export interface UpdateSettingsRequest { payment_cancel_rate_limit_window_mode?: string // Balance & quota notification balance_low_notify_enabled?: boolean + balance_low_notify_threshold_type?: 'fixed' | 'percentage' balance_low_notify_threshold?: number account_quota_notify_emails?: string[] } diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 8e10bf2a..880a81ee 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -4633,8 +4633,12 @@ export default { title: 'Balance Low Notification', description: 'Send email notification when user balance falls below threshold', enabled: 'Enable Balance Low Notification', + thresholdType: 'Threshold Type', + typeFixed: 'Fixed Amount', + typePercentage: 'Percentage of Recharged', threshold: 'Default Threshold', thresholdHint: 'Used when user has not set a custom value', + percentageHint: 'Notify when balance falls below this percentage of total recharged amount', thresholdPlaceholder: 'Enter amount', }, quotaNotify: { diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 1b82f419..41d94e06 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -4797,8 +4797,12 @@ export default { title: '余额不足提醒', description: '当用户余额低于阈值时发送邮件提醒', enabled: '启用余额不足提醒', - threshold: '默认提醒阈值', + thresholdType: '阈值类型', + typeFixed: '固定金额', + typePercentage: '充值百分比', + threshold: '提醒阈值', thresholdHint: '用户未自定义时使用此值', + percentageHint: '当余额低于累计充值额的此百分比时提醒', thresholdPlaceholder: '输入金额', }, quotaNotify: { diff --git a/frontend/src/views/admin/SettingsView.vue b/frontend/src/views/admin/SettingsView.vue index 8ed77203..af84b67d 100644 --- a/frontend/src/views/admin/SettingsView.vue +++ b/frontend/src/views/admin/SettingsView.vue @@ -2660,6 +2660,90 @@ + +
+
+

+ {{ t('admin.settings.balanceNotify.title') }} +

+

+ {{ t('admin.settings.balanceNotify.description') }} +

+
+
+
+ + +
+
+ +
+ +
+ + +
+
+ +
+ +
+ + {{ form.balance_low_notify_threshold_type === 'percentage' ? '%' : '$' }} + + +
+

+ {{ form.balance_low_notify_threshold_type === 'percentage' + ? t('admin.settings.balanceNotify.percentageHint') + : t('admin.settings.balanceNotify.thresholdHint') }} +

+
+
+
+
+ + +
+
+

+ {{ t('admin.settings.quotaNotify.title') }} +

+

+ {{ t('admin.settings.quotaNotify.description') }} +

+
+
+
+ +
+
+ + +
+ +
+

{{ t('admin.settings.quotaNotify.emailsHint') }}

+
+
+
@@ -2939,7 +3023,12 @@ const form = reactive({ // Gateway forwarding behavior enable_fingerprint_unification: true, enable_metadata_passthrough: false, - enable_cch_signing: false + enable_cch_signing: false, + // Balance & quota notification + balance_low_notify_enabled: false, + balance_low_notify_threshold_type: 'fixed' as 'fixed' | 'percentage', + balance_low_notify_threshold: 0, + account_quota_notify_emails: [] as string[] }) // Proxies for web search emulation ProxySelector @@ -3149,6 +3238,14 @@ function handleRegistrationEmailSuffixWhitelistPaste(event: ClipboardEvent) { } } +// Quota notify email helpers +const addQuotaNotifyEmail = () => { + if (!form.account_quota_notify_emails) { + form.account_quota_notify_emails = [] + } + form.account_quota_notify_emails.push('') +} + // LinuxDo OAuth redirect URL suggestion const linuxdoRedirectUrlSuggestion = computed(() => { if (typeof window === 'undefined') return '' @@ -3488,6 +3585,11 @@ async function saveSettings() { payment_cancel_rate_limit_window: Number(form.payment_cancel_rate_limit_window) || 1, payment_cancel_rate_limit_unit: form.payment_cancel_rate_limit_unit, payment_cancel_rate_limit_window_mode: form.payment_cancel_rate_limit_window_mode, + // Balance & quota notification + balance_low_notify_enabled: form.balance_low_notify_enabled, + balance_low_notify_threshold_type: form.balance_low_notify_threshold_type, + balance_low_notify_threshold: Number(form.balance_low_notify_threshold) || 0, + account_quota_notify_emails: (form.account_quota_notify_emails || []).filter((e: string) => e.trim() !== ''), } const updated = await adminAPI.settings.updateSettings(payload)