diff --git a/backend/ent/group.go b/backend/ent/group.go index 298df88abb..172b67777e 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -31,6 +31,14 @@ type Group struct { Description *string `json:"description,omitempty"` // RateMultiplier holds the value of the "rate_multiplier" field. RateMultiplier float64 `json:"rate_multiplier,omitempty"` + // 是否启用高峰时段倍率 + PeakRateEnabled bool `json:"peak_rate_enabled,omitempty"` + // 高峰开始时间 HH:MM(含),如 14:00;空表示未配置 + PeakStart string `json:"peak_start,omitempty"` + // 高峰结束时间 HH:MM(不含),如 18:00 + PeakEnd string `json:"peak_end,omitempty"` + // 高峰时段叠加倍率,仅在 peak_rate_enabled 且处于 [peak_start, peak_end) 时乘入文本倍率 + PeakRateMultiplier float64 `json:"peak_rate_multiplier,omitempty"` // IsExclusive holds the value of the "is_exclusive" field. IsExclusive bool `json:"is_exclusive,omitempty"` // Status holds the value of the "status" field. @@ -197,13 +205,13 @@ func (*Group) scanValues(columns []string) ([]any, error) { switch columns[i] { case group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig: values[i] = new([]byte) - case group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldImageRateIndependent, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet: + case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldImageRateIndependent, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet: values[i] = new(sql.NullBool) - case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImageRateMultiplier, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k: + case group.FieldRateMultiplier, group.FieldPeakRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImageRateMultiplier, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k: values[i] = new(sql.NullFloat64) case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldRpmLimit: values[i] = new(sql.NullInt64) - case group.FieldName, group.FieldDescription, group.FieldStatus, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel: + case group.FieldName, group.FieldDescription, group.FieldPeakStart, group.FieldPeakEnd, group.FieldStatus, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel: values[i] = new(sql.NullString) case group.FieldCreatedAt, group.FieldUpdatedAt, group.FieldDeletedAt: values[i] = new(sql.NullTime) @@ -266,6 +274,30 @@ func (_m *Group) assignValues(columns []string, values []any) error { } else if value.Valid { _m.RateMultiplier = value.Float64 } + case group.FieldPeakRateEnabled: + if value, ok := values[i].(*sql.NullBool); !ok { + return fmt.Errorf("unexpected type %T for field peak_rate_enabled", values[i]) + } else if value.Valid { + _m.PeakRateEnabled = value.Bool + } + case group.FieldPeakStart: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field peak_start", values[i]) + } else if value.Valid { + _m.PeakStart = value.String + } + case group.FieldPeakEnd: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field peak_end", values[i]) + } else if value.Valid { + _m.PeakEnd = value.String + } + case group.FieldPeakRateMultiplier: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field peak_rate_multiplier", values[i]) + } else if value.Valid { + _m.PeakRateMultiplier = value.Float64 + } case group.FieldIsExclusive: if value, ok := values[i].(*sql.NullBool); !ok { return fmt.Errorf("unexpected type %T for field is_exclusive", values[i]) @@ -554,6 +586,18 @@ func (_m *Group) String() string { builder.WriteString("rate_multiplier=") builder.WriteString(fmt.Sprintf("%v", _m.RateMultiplier)) builder.WriteString(", ") + builder.WriteString("peak_rate_enabled=") + builder.WriteString(fmt.Sprintf("%v", _m.PeakRateEnabled)) + builder.WriteString(", ") + builder.WriteString("peak_start=") + builder.WriteString(_m.PeakStart) + builder.WriteString(", ") + builder.WriteString("peak_end=") + builder.WriteString(_m.PeakEnd) + builder.WriteString(", ") + builder.WriteString("peak_rate_multiplier=") + builder.WriteString(fmt.Sprintf("%v", _m.PeakRateMultiplier)) + builder.WriteString(", ") builder.WriteString("is_exclusive=") builder.WriteString(fmt.Sprintf("%v", _m.IsExclusive)) builder.WriteString(", ") diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index ebe9bd7e82..bc95af71b6 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -28,6 +28,14 @@ const ( FieldDescription = "description" // FieldRateMultiplier holds the string denoting the rate_multiplier field in the database. FieldRateMultiplier = "rate_multiplier" + // FieldPeakRateEnabled holds the string denoting the peak_rate_enabled field in the database. + FieldPeakRateEnabled = "peak_rate_enabled" + // FieldPeakStart holds the string denoting the peak_start field in the database. + FieldPeakStart = "peak_start" + // FieldPeakEnd holds the string denoting the peak_end field in the database. + FieldPeakEnd = "peak_end" + // FieldPeakRateMultiplier holds the string denoting the peak_rate_multiplier field in the database. + FieldPeakRateMultiplier = "peak_rate_multiplier" // FieldIsExclusive holds the string denoting the is_exclusive field in the database. FieldIsExclusive = "is_exclusive" // FieldStatus holds the string denoting the status field in the database. @@ -167,6 +175,10 @@ var Columns = []string{ FieldName, FieldDescription, FieldRateMultiplier, + FieldPeakRateEnabled, + FieldPeakStart, + FieldPeakEnd, + FieldPeakRateMultiplier, FieldIsExclusive, FieldStatus, FieldPlatform, @@ -235,6 +247,18 @@ var ( NameValidator func(string) error // DefaultRateMultiplier holds the default value on creation for the "rate_multiplier" field. DefaultRateMultiplier float64 + // DefaultPeakRateEnabled holds the default value on creation for the "peak_rate_enabled" field. + DefaultPeakRateEnabled bool + // DefaultPeakStart holds the default value on creation for the "peak_start" field. + DefaultPeakStart string + // PeakStartValidator is a validator for the "peak_start" field. It is called by the builders before save. + PeakStartValidator func(string) error + // DefaultPeakEnd holds the default value on creation for the "peak_end" field. + DefaultPeakEnd string + // PeakEndValidator is a validator for the "peak_end" field. It is called by the builders before save. + PeakEndValidator func(string) error + // DefaultPeakRateMultiplier holds the default value on creation for the "peak_rate_multiplier" field. + DefaultPeakRateMultiplier float64 // DefaultIsExclusive holds the default value on creation for the "is_exclusive" field. DefaultIsExclusive bool // DefaultStatus holds the default value on creation for the "status" field. @@ -323,6 +347,26 @@ func ByRateMultiplier(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldRateMultiplier, opts...).ToFunc() } +// ByPeakRateEnabled orders the results by the peak_rate_enabled field. +func ByPeakRateEnabled(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPeakRateEnabled, opts...).ToFunc() +} + +// ByPeakStart orders the results by the peak_start field. +func ByPeakStart(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPeakStart, opts...).ToFunc() +} + +// ByPeakEnd orders the results by the peak_end field. +func ByPeakEnd(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPeakEnd, opts...).ToFunc() +} + +// ByPeakRateMultiplier orders the results by the peak_rate_multiplier field. +func ByPeakRateMultiplier(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldPeakRateMultiplier, opts...).ToFunc() +} + // ByIsExclusive orders the results by the is_exclusive field. func ByIsExclusive(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldIsExclusive, opts...).ToFunc() diff --git a/backend/ent/group/where.go b/backend/ent/group/where.go index d3223a9257..4a7fc01991 100644 --- a/backend/ent/group/where.go +++ b/backend/ent/group/where.go @@ -85,6 +85,26 @@ func RateMultiplier(v float64) predicate.Group { return predicate.Group(sql.FieldEQ(FieldRateMultiplier, v)) } +// PeakRateEnabled applies equality check predicate on the "peak_rate_enabled" field. It's identical to PeakRateEnabledEQ. +func PeakRateEnabled(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakRateEnabled, v)) +} + +// PeakStart applies equality check predicate on the "peak_start" field. It's identical to PeakStartEQ. +func PeakStart(v string) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakStart, v)) +} + +// PeakEnd applies equality check predicate on the "peak_end" field. It's identical to PeakEndEQ. +func PeakEnd(v string) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakEnd, v)) +} + +// PeakRateMultiplier applies equality check predicate on the "peak_rate_multiplier" field. It's identical to PeakRateMultiplierEQ. +func PeakRateMultiplier(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakRateMultiplier, v)) +} + // IsExclusive applies equality check predicate on the "is_exclusive" field. It's identical to IsExclusiveEQ. func IsExclusive(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldIsExclusive, v)) @@ -520,6 +540,186 @@ func RateMultiplierLTE(v float64) predicate.Group { return predicate.Group(sql.FieldLTE(FieldRateMultiplier, v)) } +// PeakRateEnabledEQ applies the EQ predicate on the "peak_rate_enabled" field. +func PeakRateEnabledEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakRateEnabled, v)) +} + +// PeakRateEnabledNEQ applies the NEQ predicate on the "peak_rate_enabled" field. +func PeakRateEnabledNEQ(v bool) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldPeakRateEnabled, v)) +} + +// PeakStartEQ applies the EQ predicate on the "peak_start" field. +func PeakStartEQ(v string) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakStart, v)) +} + +// PeakStartNEQ applies the NEQ predicate on the "peak_start" field. +func PeakStartNEQ(v string) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldPeakStart, v)) +} + +// PeakStartIn applies the In predicate on the "peak_start" field. +func PeakStartIn(vs ...string) predicate.Group { + return predicate.Group(sql.FieldIn(FieldPeakStart, vs...)) +} + +// PeakStartNotIn applies the NotIn predicate on the "peak_start" field. +func PeakStartNotIn(vs ...string) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldPeakStart, vs...)) +} + +// PeakStartGT applies the GT predicate on the "peak_start" field. +func PeakStartGT(v string) predicate.Group { + return predicate.Group(sql.FieldGT(FieldPeakStart, v)) +} + +// PeakStartGTE applies the GTE predicate on the "peak_start" field. +func PeakStartGTE(v string) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldPeakStart, v)) +} + +// PeakStartLT applies the LT predicate on the "peak_start" field. +func PeakStartLT(v string) predicate.Group { + return predicate.Group(sql.FieldLT(FieldPeakStart, v)) +} + +// PeakStartLTE applies the LTE predicate on the "peak_start" field. +func PeakStartLTE(v string) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldPeakStart, v)) +} + +// PeakStartContains applies the Contains predicate on the "peak_start" field. +func PeakStartContains(v string) predicate.Group { + return predicate.Group(sql.FieldContains(FieldPeakStart, v)) +} + +// PeakStartHasPrefix applies the HasPrefix predicate on the "peak_start" field. +func PeakStartHasPrefix(v string) predicate.Group { + return predicate.Group(sql.FieldHasPrefix(FieldPeakStart, v)) +} + +// PeakStartHasSuffix applies the HasSuffix predicate on the "peak_start" field. +func PeakStartHasSuffix(v string) predicate.Group { + return predicate.Group(sql.FieldHasSuffix(FieldPeakStart, v)) +} + +// PeakStartEqualFold applies the EqualFold predicate on the "peak_start" field. +func PeakStartEqualFold(v string) predicate.Group { + return predicate.Group(sql.FieldEqualFold(FieldPeakStart, v)) +} + +// PeakStartContainsFold applies the ContainsFold predicate on the "peak_start" field. +func PeakStartContainsFold(v string) predicate.Group { + return predicate.Group(sql.FieldContainsFold(FieldPeakStart, v)) +} + +// PeakEndEQ applies the EQ predicate on the "peak_end" field. +func PeakEndEQ(v string) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakEnd, v)) +} + +// PeakEndNEQ applies the NEQ predicate on the "peak_end" field. +func PeakEndNEQ(v string) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldPeakEnd, v)) +} + +// PeakEndIn applies the In predicate on the "peak_end" field. +func PeakEndIn(vs ...string) predicate.Group { + return predicate.Group(sql.FieldIn(FieldPeakEnd, vs...)) +} + +// PeakEndNotIn applies the NotIn predicate on the "peak_end" field. +func PeakEndNotIn(vs ...string) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldPeakEnd, vs...)) +} + +// PeakEndGT applies the GT predicate on the "peak_end" field. +func PeakEndGT(v string) predicate.Group { + return predicate.Group(sql.FieldGT(FieldPeakEnd, v)) +} + +// PeakEndGTE applies the GTE predicate on the "peak_end" field. +func PeakEndGTE(v string) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldPeakEnd, v)) +} + +// PeakEndLT applies the LT predicate on the "peak_end" field. +func PeakEndLT(v string) predicate.Group { + return predicate.Group(sql.FieldLT(FieldPeakEnd, v)) +} + +// PeakEndLTE applies the LTE predicate on the "peak_end" field. +func PeakEndLTE(v string) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldPeakEnd, v)) +} + +// PeakEndContains applies the Contains predicate on the "peak_end" field. +func PeakEndContains(v string) predicate.Group { + return predicate.Group(sql.FieldContains(FieldPeakEnd, v)) +} + +// PeakEndHasPrefix applies the HasPrefix predicate on the "peak_end" field. +func PeakEndHasPrefix(v string) predicate.Group { + return predicate.Group(sql.FieldHasPrefix(FieldPeakEnd, v)) +} + +// PeakEndHasSuffix applies the HasSuffix predicate on the "peak_end" field. +func PeakEndHasSuffix(v string) predicate.Group { + return predicate.Group(sql.FieldHasSuffix(FieldPeakEnd, v)) +} + +// PeakEndEqualFold applies the EqualFold predicate on the "peak_end" field. +func PeakEndEqualFold(v string) predicate.Group { + return predicate.Group(sql.FieldEqualFold(FieldPeakEnd, v)) +} + +// PeakEndContainsFold applies the ContainsFold predicate on the "peak_end" field. +func PeakEndContainsFold(v string) predicate.Group { + return predicate.Group(sql.FieldContainsFold(FieldPeakEnd, v)) +} + +// PeakRateMultiplierEQ applies the EQ predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldPeakRateMultiplier, v)) +} + +// PeakRateMultiplierNEQ applies the NEQ predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldPeakRateMultiplier, v)) +} + +// PeakRateMultiplierIn applies the In predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldPeakRateMultiplier, vs...)) +} + +// PeakRateMultiplierNotIn applies the NotIn predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldPeakRateMultiplier, vs...)) +} + +// PeakRateMultiplierGT applies the GT predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldPeakRateMultiplier, v)) +} + +// PeakRateMultiplierGTE applies the GTE predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldPeakRateMultiplier, v)) +} + +// PeakRateMultiplierLT applies the LT predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldPeakRateMultiplier, v)) +} + +// PeakRateMultiplierLTE applies the LTE predicate on the "peak_rate_multiplier" field. +func PeakRateMultiplierLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldPeakRateMultiplier, v)) +} + // IsExclusiveEQ applies the EQ predicate on the "is_exclusive" field. func IsExclusiveEQ(v bool) predicate.Group { return predicate.Group(sql.FieldEQ(FieldIsExclusive, v)) diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index d5ed0c19a4..0f35f070ee 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -105,6 +105,62 @@ func (_c *GroupCreate) SetNillableRateMultiplier(v *float64) *GroupCreate { return _c } +// SetPeakRateEnabled sets the "peak_rate_enabled" field. +func (_c *GroupCreate) SetPeakRateEnabled(v bool) *GroupCreate { + _c.mutation.SetPeakRateEnabled(v) + return _c +} + +// SetNillablePeakRateEnabled sets the "peak_rate_enabled" field if the given value is not nil. +func (_c *GroupCreate) SetNillablePeakRateEnabled(v *bool) *GroupCreate { + if v != nil { + _c.SetPeakRateEnabled(*v) + } + return _c +} + +// SetPeakStart sets the "peak_start" field. +func (_c *GroupCreate) SetPeakStart(v string) *GroupCreate { + _c.mutation.SetPeakStart(v) + return _c +} + +// SetNillablePeakStart sets the "peak_start" field if the given value is not nil. +func (_c *GroupCreate) SetNillablePeakStart(v *string) *GroupCreate { + if v != nil { + _c.SetPeakStart(*v) + } + return _c +} + +// SetPeakEnd sets the "peak_end" field. +func (_c *GroupCreate) SetPeakEnd(v string) *GroupCreate { + _c.mutation.SetPeakEnd(v) + return _c +} + +// SetNillablePeakEnd sets the "peak_end" field if the given value is not nil. +func (_c *GroupCreate) SetNillablePeakEnd(v *string) *GroupCreate { + if v != nil { + _c.SetPeakEnd(*v) + } + return _c +} + +// SetPeakRateMultiplier sets the "peak_rate_multiplier" field. +func (_c *GroupCreate) SetPeakRateMultiplier(v float64) *GroupCreate { + _c.mutation.SetPeakRateMultiplier(v) + return _c +} + +// SetNillablePeakRateMultiplier sets the "peak_rate_multiplier" field if the given value is not nil. +func (_c *GroupCreate) SetNillablePeakRateMultiplier(v *float64) *GroupCreate { + if v != nil { + _c.SetPeakRateMultiplier(*v) + } + return _c +} + // SetIsExclusive sets the "is_exclusive" field. func (_c *GroupCreate) SetIsExclusive(v bool) *GroupCreate { _c.mutation.SetIsExclusive(v) @@ -640,6 +696,22 @@ func (_c *GroupCreate) defaults() error { v := group.DefaultRateMultiplier _c.mutation.SetRateMultiplier(v) } + if _, ok := _c.mutation.PeakRateEnabled(); !ok { + v := group.DefaultPeakRateEnabled + _c.mutation.SetPeakRateEnabled(v) + } + if _, ok := _c.mutation.PeakStart(); !ok { + v := group.DefaultPeakStart + _c.mutation.SetPeakStart(v) + } + if _, ok := _c.mutation.PeakEnd(); !ok { + v := group.DefaultPeakEnd + _c.mutation.SetPeakEnd(v) + } + if _, ok := _c.mutation.PeakRateMultiplier(); !ok { + v := group.DefaultPeakRateMultiplier + _c.mutation.SetPeakRateMultiplier(v) + } if _, ok := _c.mutation.IsExclusive(); !ok { v := group.DefaultIsExclusive _c.mutation.SetIsExclusive(v) @@ -742,6 +814,28 @@ func (_c *GroupCreate) check() error { if _, ok := _c.mutation.RateMultiplier(); !ok { return &ValidationError{Name: "rate_multiplier", err: errors.New(`ent: missing required field "Group.rate_multiplier"`)} } + if _, ok := _c.mutation.PeakRateEnabled(); !ok { + return &ValidationError{Name: "peak_rate_enabled", err: errors.New(`ent: missing required field "Group.peak_rate_enabled"`)} + } + if _, ok := _c.mutation.PeakStart(); !ok { + return &ValidationError{Name: "peak_start", err: errors.New(`ent: missing required field "Group.peak_start"`)} + } + if v, ok := _c.mutation.PeakStart(); ok { + if err := group.PeakStartValidator(v); err != nil { + return &ValidationError{Name: "peak_start", err: fmt.Errorf(`ent: validator failed for field "Group.peak_start": %w`, err)} + } + } + if _, ok := _c.mutation.PeakEnd(); !ok { + return &ValidationError{Name: "peak_end", err: errors.New(`ent: missing required field "Group.peak_end"`)} + } + if v, ok := _c.mutation.PeakEnd(); ok { + if err := group.PeakEndValidator(v); err != nil { + return &ValidationError{Name: "peak_end", err: fmt.Errorf(`ent: validator failed for field "Group.peak_end": %w`, err)} + } + } + if _, ok := _c.mutation.PeakRateMultiplier(); !ok { + return &ValidationError{Name: "peak_rate_multiplier", err: errors.New(`ent: missing required field "Group.peak_rate_multiplier"`)} + } if _, ok := _c.mutation.IsExclusive(); !ok { return &ValidationError{Name: "is_exclusive", err: errors.New(`ent: missing required field "Group.is_exclusive"`)} } @@ -873,6 +967,22 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _spec.SetField(group.FieldRateMultiplier, field.TypeFloat64, value) _node.RateMultiplier = value } + if value, ok := _c.mutation.PeakRateEnabled(); ok { + _spec.SetField(group.FieldPeakRateEnabled, field.TypeBool, value) + _node.PeakRateEnabled = value + } + if value, ok := _c.mutation.PeakStart(); ok { + _spec.SetField(group.FieldPeakStart, field.TypeString, value) + _node.PeakStart = value + } + if value, ok := _c.mutation.PeakEnd(); ok { + _spec.SetField(group.FieldPeakEnd, field.TypeString, value) + _node.PeakEnd = value + } + if value, ok := _c.mutation.PeakRateMultiplier(); ok { + _spec.SetField(group.FieldPeakRateMultiplier, field.TypeFloat64, value) + _node.PeakRateMultiplier = value + } if value, ok := _c.mutation.IsExclusive(); ok { _spec.SetField(group.FieldIsExclusive, field.TypeBool, value) _node.IsExclusive = value @@ -1223,6 +1333,60 @@ func (u *GroupUpsert) AddRateMultiplier(v float64) *GroupUpsert { return u } +// SetPeakRateEnabled sets the "peak_rate_enabled" field. +func (u *GroupUpsert) SetPeakRateEnabled(v bool) *GroupUpsert { + u.Set(group.FieldPeakRateEnabled, v) + return u +} + +// UpdatePeakRateEnabled sets the "peak_rate_enabled" field to the value that was provided on create. +func (u *GroupUpsert) UpdatePeakRateEnabled() *GroupUpsert { + u.SetExcluded(group.FieldPeakRateEnabled) + return u +} + +// SetPeakStart sets the "peak_start" field. +func (u *GroupUpsert) SetPeakStart(v string) *GroupUpsert { + u.Set(group.FieldPeakStart, v) + return u +} + +// UpdatePeakStart sets the "peak_start" field to the value that was provided on create. +func (u *GroupUpsert) UpdatePeakStart() *GroupUpsert { + u.SetExcluded(group.FieldPeakStart) + return u +} + +// SetPeakEnd sets the "peak_end" field. +func (u *GroupUpsert) SetPeakEnd(v string) *GroupUpsert { + u.Set(group.FieldPeakEnd, v) + return u +} + +// UpdatePeakEnd sets the "peak_end" field to the value that was provided on create. +func (u *GroupUpsert) UpdatePeakEnd() *GroupUpsert { + u.SetExcluded(group.FieldPeakEnd) + return u +} + +// SetPeakRateMultiplier sets the "peak_rate_multiplier" field. +func (u *GroupUpsert) SetPeakRateMultiplier(v float64) *GroupUpsert { + u.Set(group.FieldPeakRateMultiplier, v) + return u +} + +// UpdatePeakRateMultiplier sets the "peak_rate_multiplier" field to the value that was provided on create. +func (u *GroupUpsert) UpdatePeakRateMultiplier() *GroupUpsert { + u.SetExcluded(group.FieldPeakRateMultiplier) + return u +} + +// AddPeakRateMultiplier adds v to the "peak_rate_multiplier" field. +func (u *GroupUpsert) AddPeakRateMultiplier(v float64) *GroupUpsert { + u.Add(group.FieldPeakRateMultiplier, v) + return u +} + // SetIsExclusive sets the "is_exclusive" field. func (u *GroupUpsert) SetIsExclusive(v bool) *GroupUpsert { u.Set(group.FieldIsExclusive, v) @@ -1833,6 +1997,69 @@ func (u *GroupUpsertOne) UpdateRateMultiplier() *GroupUpsertOne { }) } +// SetPeakRateEnabled sets the "peak_rate_enabled" field. +func (u *GroupUpsertOne) SetPeakRateEnabled(v bool) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetPeakRateEnabled(v) + }) +} + +// UpdatePeakRateEnabled sets the "peak_rate_enabled" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdatePeakRateEnabled() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakRateEnabled() + }) +} + +// SetPeakStart sets the "peak_start" field. +func (u *GroupUpsertOne) SetPeakStart(v string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetPeakStart(v) + }) +} + +// UpdatePeakStart sets the "peak_start" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdatePeakStart() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakStart() + }) +} + +// SetPeakEnd sets the "peak_end" field. +func (u *GroupUpsertOne) SetPeakEnd(v string) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetPeakEnd(v) + }) +} + +// UpdatePeakEnd sets the "peak_end" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdatePeakEnd() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakEnd() + }) +} + +// SetPeakRateMultiplier sets the "peak_rate_multiplier" field. +func (u *GroupUpsertOne) SetPeakRateMultiplier(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetPeakRateMultiplier(v) + }) +} + +// AddPeakRateMultiplier adds v to the "peak_rate_multiplier" field. +func (u *GroupUpsertOne) AddPeakRateMultiplier(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddPeakRateMultiplier(v) + }) +} + +// UpdatePeakRateMultiplier sets the "peak_rate_multiplier" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdatePeakRateMultiplier() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakRateMultiplier() + }) +} + // SetIsExclusive sets the "is_exclusive" field. func (u *GroupUpsertOne) SetIsExclusive(v bool) *GroupUpsertOne { return u.Update(func(s *GroupUpsert) { @@ -2688,6 +2915,69 @@ func (u *GroupUpsertBulk) UpdateRateMultiplier() *GroupUpsertBulk { }) } +// SetPeakRateEnabled sets the "peak_rate_enabled" field. +func (u *GroupUpsertBulk) SetPeakRateEnabled(v bool) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetPeakRateEnabled(v) + }) +} + +// UpdatePeakRateEnabled sets the "peak_rate_enabled" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdatePeakRateEnabled() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakRateEnabled() + }) +} + +// SetPeakStart sets the "peak_start" field. +func (u *GroupUpsertBulk) SetPeakStart(v string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetPeakStart(v) + }) +} + +// UpdatePeakStart sets the "peak_start" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdatePeakStart() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakStart() + }) +} + +// SetPeakEnd sets the "peak_end" field. +func (u *GroupUpsertBulk) SetPeakEnd(v string) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetPeakEnd(v) + }) +} + +// UpdatePeakEnd sets the "peak_end" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdatePeakEnd() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakEnd() + }) +} + +// SetPeakRateMultiplier sets the "peak_rate_multiplier" field. +func (u *GroupUpsertBulk) SetPeakRateMultiplier(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetPeakRateMultiplier(v) + }) +} + +// AddPeakRateMultiplier adds v to the "peak_rate_multiplier" field. +func (u *GroupUpsertBulk) AddPeakRateMultiplier(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddPeakRateMultiplier(v) + }) +} + +// UpdatePeakRateMultiplier sets the "peak_rate_multiplier" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdatePeakRateMultiplier() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdatePeakRateMultiplier() + }) +} + // SetIsExclusive sets the "is_exclusive" field. func (u *GroupUpsertBulk) SetIsExclusive(v bool) *GroupUpsertBulk { return u.Update(func(s *GroupUpsert) { diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index c10d60ecb8..55555f323c 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -117,6 +117,69 @@ func (_u *GroupUpdate) AddRateMultiplier(v float64) *GroupUpdate { return _u } +// SetPeakRateEnabled sets the "peak_rate_enabled" field. +func (_u *GroupUpdate) SetPeakRateEnabled(v bool) *GroupUpdate { + _u.mutation.SetPeakRateEnabled(v) + return _u +} + +// SetNillablePeakRateEnabled sets the "peak_rate_enabled" field if the given value is not nil. +func (_u *GroupUpdate) SetNillablePeakRateEnabled(v *bool) *GroupUpdate { + if v != nil { + _u.SetPeakRateEnabled(*v) + } + return _u +} + +// SetPeakStart sets the "peak_start" field. +func (_u *GroupUpdate) SetPeakStart(v string) *GroupUpdate { + _u.mutation.SetPeakStart(v) + return _u +} + +// SetNillablePeakStart sets the "peak_start" field if the given value is not nil. +func (_u *GroupUpdate) SetNillablePeakStart(v *string) *GroupUpdate { + if v != nil { + _u.SetPeakStart(*v) + } + return _u +} + +// SetPeakEnd sets the "peak_end" field. +func (_u *GroupUpdate) SetPeakEnd(v string) *GroupUpdate { + _u.mutation.SetPeakEnd(v) + return _u +} + +// SetNillablePeakEnd sets the "peak_end" field if the given value is not nil. +func (_u *GroupUpdate) SetNillablePeakEnd(v *string) *GroupUpdate { + if v != nil { + _u.SetPeakEnd(*v) + } + return _u +} + +// SetPeakRateMultiplier sets the "peak_rate_multiplier" field. +func (_u *GroupUpdate) SetPeakRateMultiplier(v float64) *GroupUpdate { + _u.mutation.ResetPeakRateMultiplier() + _u.mutation.SetPeakRateMultiplier(v) + return _u +} + +// SetNillablePeakRateMultiplier sets the "peak_rate_multiplier" field if the given value is not nil. +func (_u *GroupUpdate) SetNillablePeakRateMultiplier(v *float64) *GroupUpdate { + if v != nil { + _u.SetPeakRateMultiplier(*v) + } + return _u +} + +// AddPeakRateMultiplier adds value to the "peak_rate_multiplier" field. +func (_u *GroupUpdate) AddPeakRateMultiplier(v float64) *GroupUpdate { + _u.mutation.AddPeakRateMultiplier(v) + return _u +} + // SetIsExclusive sets the "is_exclusive" field. func (_u *GroupUpdate) SetIsExclusive(v bool) *GroupUpdate { _u.mutation.SetIsExclusive(v) @@ -921,6 +984,16 @@ func (_u *GroupUpdate) check() error { return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Group.name": %w`, err)} } } + if v, ok := _u.mutation.PeakStart(); ok { + if err := group.PeakStartValidator(v); err != nil { + return &ValidationError{Name: "peak_start", err: fmt.Errorf(`ent: validator failed for field "Group.peak_start": %w`, err)} + } + } + if v, ok := _u.mutation.PeakEnd(); ok { + if err := group.PeakEndValidator(v); err != nil { + return &ValidationError{Name: "peak_end", err: fmt.Errorf(`ent: validator failed for field "Group.peak_end": %w`, err)} + } + } if v, ok := _u.mutation.Status(); ok { if err := group.StatusValidator(v); err != nil { return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Group.status": %w`, err)} @@ -980,6 +1053,21 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.AddedRateMultiplier(); ok { _spec.AddField(group.FieldRateMultiplier, field.TypeFloat64, value) } + if value, ok := _u.mutation.PeakRateEnabled(); ok { + _spec.SetField(group.FieldPeakRateEnabled, field.TypeBool, value) + } + if value, ok := _u.mutation.PeakStart(); ok { + _spec.SetField(group.FieldPeakStart, field.TypeString, value) + } + if value, ok := _u.mutation.PeakEnd(); ok { + _spec.SetField(group.FieldPeakEnd, field.TypeString, value) + } + if value, ok := _u.mutation.PeakRateMultiplier(); ok { + _spec.SetField(group.FieldPeakRateMultiplier, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedPeakRateMultiplier(); ok { + _spec.AddField(group.FieldPeakRateMultiplier, field.TypeFloat64, value) + } if value, ok := _u.mutation.IsExclusive(); ok { _spec.SetField(group.FieldIsExclusive, field.TypeBool, value) } @@ -1530,6 +1618,69 @@ func (_u *GroupUpdateOne) AddRateMultiplier(v float64) *GroupUpdateOne { return _u } +// SetPeakRateEnabled sets the "peak_rate_enabled" field. +func (_u *GroupUpdateOne) SetPeakRateEnabled(v bool) *GroupUpdateOne { + _u.mutation.SetPeakRateEnabled(v) + return _u +} + +// SetNillablePeakRateEnabled sets the "peak_rate_enabled" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillablePeakRateEnabled(v *bool) *GroupUpdateOne { + if v != nil { + _u.SetPeakRateEnabled(*v) + } + return _u +} + +// SetPeakStart sets the "peak_start" field. +func (_u *GroupUpdateOne) SetPeakStart(v string) *GroupUpdateOne { + _u.mutation.SetPeakStart(v) + return _u +} + +// SetNillablePeakStart sets the "peak_start" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillablePeakStart(v *string) *GroupUpdateOne { + if v != nil { + _u.SetPeakStart(*v) + } + return _u +} + +// SetPeakEnd sets the "peak_end" field. +func (_u *GroupUpdateOne) SetPeakEnd(v string) *GroupUpdateOne { + _u.mutation.SetPeakEnd(v) + return _u +} + +// SetNillablePeakEnd sets the "peak_end" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillablePeakEnd(v *string) *GroupUpdateOne { + if v != nil { + _u.SetPeakEnd(*v) + } + return _u +} + +// SetPeakRateMultiplier sets the "peak_rate_multiplier" field. +func (_u *GroupUpdateOne) SetPeakRateMultiplier(v float64) *GroupUpdateOne { + _u.mutation.ResetPeakRateMultiplier() + _u.mutation.SetPeakRateMultiplier(v) + return _u +} + +// SetNillablePeakRateMultiplier sets the "peak_rate_multiplier" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillablePeakRateMultiplier(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetPeakRateMultiplier(*v) + } + return _u +} + +// AddPeakRateMultiplier adds value to the "peak_rate_multiplier" field. +func (_u *GroupUpdateOne) AddPeakRateMultiplier(v float64) *GroupUpdateOne { + _u.mutation.AddPeakRateMultiplier(v) + return _u +} + // SetIsExclusive sets the "is_exclusive" field. func (_u *GroupUpdateOne) SetIsExclusive(v bool) *GroupUpdateOne { _u.mutation.SetIsExclusive(v) @@ -2347,6 +2498,16 @@ func (_u *GroupUpdateOne) check() error { return &ValidationError{Name: "name", err: fmt.Errorf(`ent: validator failed for field "Group.name": %w`, err)} } } + if v, ok := _u.mutation.PeakStart(); ok { + if err := group.PeakStartValidator(v); err != nil { + return &ValidationError{Name: "peak_start", err: fmt.Errorf(`ent: validator failed for field "Group.peak_start": %w`, err)} + } + } + if v, ok := _u.mutation.PeakEnd(); ok { + if err := group.PeakEndValidator(v); err != nil { + return &ValidationError{Name: "peak_end", err: fmt.Errorf(`ent: validator failed for field "Group.peak_end": %w`, err)} + } + } if v, ok := _u.mutation.Status(); ok { if err := group.StatusValidator(v); err != nil { return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Group.status": %w`, err)} @@ -2423,6 +2584,21 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error) if value, ok := _u.mutation.AddedRateMultiplier(); ok { _spec.AddField(group.FieldRateMultiplier, field.TypeFloat64, value) } + if value, ok := _u.mutation.PeakRateEnabled(); ok { + _spec.SetField(group.FieldPeakRateEnabled, field.TypeBool, value) + } + if value, ok := _u.mutation.PeakStart(); ok { + _spec.SetField(group.FieldPeakStart, field.TypeString, value) + } + if value, ok := _u.mutation.PeakEnd(); ok { + _spec.SetField(group.FieldPeakEnd, field.TypeString, value) + } + if value, ok := _u.mutation.PeakRateMultiplier(); ok { + _spec.SetField(group.FieldPeakRateMultiplier, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedPeakRateMultiplier(); ok { + _spec.AddField(group.FieldPeakRateMultiplier, field.TypeFloat64, value) + } if value, ok := _u.mutation.IsExclusive(); ok { _spec.SetField(group.FieldIsExclusive, field.TypeBool, value) } diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index d5e9130416..d48d988a5b 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -644,6 +644,10 @@ var ( {Name: "name", Type: field.TypeString, Size: 100}, {Name: "description", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}}, {Name: "rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}}, + {Name: "peak_rate_enabled", Type: field.TypeBool, Default: false}, + {Name: "peak_start", Type: field.TypeString, Size: 5, Default: ""}, + {Name: "peak_end", Type: field.TypeString, Size: 5, Default: ""}, + {Name: "peak_rate_multiplier", Type: field.TypeFloat64, Default: 1, SchemaType: map[string]string{"postgres": "decimal(10,4)"}}, {Name: "is_exclusive", Type: field.TypeBool, Default: false}, {Name: "status", Type: field.TypeString, Size: 20, Default: "active"}, {Name: "platform", Type: field.TypeString, Size: 50, Default: "anthropic"}, @@ -683,22 +687,22 @@ var ( { Name: "group_status", Unique: false, - Columns: []*schema.Column{GroupsColumns[8]}, + Columns: []*schema.Column{GroupsColumns[12]}, }, { Name: "group_platform", Unique: false, - Columns: []*schema.Column{GroupsColumns[9]}, + Columns: []*schema.Column{GroupsColumns[13]}, }, { Name: "group_subscription_type", Unique: false, - Columns: []*schema.Column{GroupsColumns[10]}, + Columns: []*schema.Column{GroupsColumns[14]}, }, { Name: "group_is_exclusive", Unique: false, - Columns: []*schema.Column{GroupsColumns[7]}, + Columns: []*schema.Column{GroupsColumns[11]}, }, { Name: "group_deleted_at", @@ -708,7 +712,7 @@ var ( { Name: "group_sort_order", Unique: false, - Columns: []*schema.Column{GroupsColumns[28]}, + Columns: []*schema.Column{GroupsColumns[32]}, }, }, } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index d62620bc7b..9b5ab14512 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -15056,6 +15056,11 @@ type GroupMutation struct { description *string rate_multiplier *float64 addrate_multiplier *float64 + peak_rate_enabled *bool + peak_start *string + peak_end *string + peak_rate_multiplier *float64 + addpeak_rate_multiplier *float64 is_exclusive *bool status *string platform *string @@ -15482,6 +15487,170 @@ func (m *GroupMutation) ResetRateMultiplier() { m.addrate_multiplier = nil } +// SetPeakRateEnabled sets the "peak_rate_enabled" field. +func (m *GroupMutation) SetPeakRateEnabled(b bool) { + m.peak_rate_enabled = &b +} + +// PeakRateEnabled returns the value of the "peak_rate_enabled" field in the mutation. +func (m *GroupMutation) PeakRateEnabled() (r bool, exists bool) { + v := m.peak_rate_enabled + if v == nil { + return + } + return *v, true +} + +// OldPeakRateEnabled returns the old "peak_rate_enabled" field's value of the Group entity. +// If the Group 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 *GroupMutation) OldPeakRateEnabled(ctx context.Context) (v bool, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPeakRateEnabled is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPeakRateEnabled requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPeakRateEnabled: %w", err) + } + return oldValue.PeakRateEnabled, nil +} + +// ResetPeakRateEnabled resets all changes to the "peak_rate_enabled" field. +func (m *GroupMutation) ResetPeakRateEnabled() { + m.peak_rate_enabled = nil +} + +// SetPeakStart sets the "peak_start" field. +func (m *GroupMutation) SetPeakStart(s string) { + m.peak_start = &s +} + +// PeakStart returns the value of the "peak_start" field in the mutation. +func (m *GroupMutation) PeakStart() (r string, exists bool) { + v := m.peak_start + if v == nil { + return + } + return *v, true +} + +// OldPeakStart returns the old "peak_start" field's value of the Group entity. +// If the Group 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 *GroupMutation) OldPeakStart(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPeakStart is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPeakStart requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPeakStart: %w", err) + } + return oldValue.PeakStart, nil +} + +// ResetPeakStart resets all changes to the "peak_start" field. +func (m *GroupMutation) ResetPeakStart() { + m.peak_start = nil +} + +// SetPeakEnd sets the "peak_end" field. +func (m *GroupMutation) SetPeakEnd(s string) { + m.peak_end = &s +} + +// PeakEnd returns the value of the "peak_end" field in the mutation. +func (m *GroupMutation) PeakEnd() (r string, exists bool) { + v := m.peak_end + if v == nil { + return + } + return *v, true +} + +// OldPeakEnd returns the old "peak_end" field's value of the Group entity. +// If the Group 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 *GroupMutation) OldPeakEnd(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPeakEnd is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPeakEnd requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPeakEnd: %w", err) + } + return oldValue.PeakEnd, nil +} + +// ResetPeakEnd resets all changes to the "peak_end" field. +func (m *GroupMutation) ResetPeakEnd() { + m.peak_end = nil +} + +// SetPeakRateMultiplier sets the "peak_rate_multiplier" field. +func (m *GroupMutation) SetPeakRateMultiplier(f float64) { + m.peak_rate_multiplier = &f + m.addpeak_rate_multiplier = nil +} + +// PeakRateMultiplier returns the value of the "peak_rate_multiplier" field in the mutation. +func (m *GroupMutation) PeakRateMultiplier() (r float64, exists bool) { + v := m.peak_rate_multiplier + if v == nil { + return + } + return *v, true +} + +// OldPeakRateMultiplier returns the old "peak_rate_multiplier" field's value of the Group entity. +// If the Group 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 *GroupMutation) OldPeakRateMultiplier(ctx context.Context) (v float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldPeakRateMultiplier is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldPeakRateMultiplier requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldPeakRateMultiplier: %w", err) + } + return oldValue.PeakRateMultiplier, nil +} + +// AddPeakRateMultiplier adds f to the "peak_rate_multiplier" field. +func (m *GroupMutation) AddPeakRateMultiplier(f float64) { + if m.addpeak_rate_multiplier != nil { + *m.addpeak_rate_multiplier += f + } else { + m.addpeak_rate_multiplier = &f + } +} + +// AddedPeakRateMultiplier returns the value that was added to the "peak_rate_multiplier" field in this mutation. +func (m *GroupMutation) AddedPeakRateMultiplier() (r float64, exists bool) { + v := m.addpeak_rate_multiplier + if v == nil { + return + } + return *v, true +} + +// ResetPeakRateMultiplier resets all changes to the "peak_rate_multiplier" field. +func (m *GroupMutation) ResetPeakRateMultiplier() { + m.peak_rate_multiplier = nil + m.addpeak_rate_multiplier = nil +} + // SetIsExclusive sets the "is_exclusive" field. func (m *GroupMutation) SetIsExclusive(b bool) { m.is_exclusive = &b @@ -17264,7 +17433,7 @@ func (m *GroupMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *GroupMutation) Fields() []string { - fields := make([]string, 0, 35) + fields := make([]string, 0, 39) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -17283,6 +17452,18 @@ func (m *GroupMutation) Fields() []string { if m.rate_multiplier != nil { fields = append(fields, group.FieldRateMultiplier) } + if m.peak_rate_enabled != nil { + fields = append(fields, group.FieldPeakRateEnabled) + } + if m.peak_start != nil { + fields = append(fields, group.FieldPeakStart) + } + if m.peak_end != nil { + fields = append(fields, group.FieldPeakEnd) + } + if m.peak_rate_multiplier != nil { + fields = append(fields, group.FieldPeakRateMultiplier) + } if m.is_exclusive != nil { fields = append(fields, group.FieldIsExclusive) } @@ -17390,6 +17571,14 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.Description() case group.FieldRateMultiplier: return m.RateMultiplier() + case group.FieldPeakRateEnabled: + return m.PeakRateEnabled() + case group.FieldPeakStart: + return m.PeakStart() + case group.FieldPeakEnd: + return m.PeakEnd() + case group.FieldPeakRateMultiplier: + return m.PeakRateMultiplier() case group.FieldIsExclusive: return m.IsExclusive() case group.FieldStatus: @@ -17469,6 +17658,14 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldDescription(ctx) case group.FieldRateMultiplier: return m.OldRateMultiplier(ctx) + case group.FieldPeakRateEnabled: + return m.OldPeakRateEnabled(ctx) + case group.FieldPeakStart: + return m.OldPeakStart(ctx) + case group.FieldPeakEnd: + return m.OldPeakEnd(ctx) + case group.FieldPeakRateMultiplier: + return m.OldPeakRateMultiplier(ctx) case group.FieldIsExclusive: return m.OldIsExclusive(ctx) case group.FieldStatus: @@ -17578,6 +17775,34 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetRateMultiplier(v) return nil + case group.FieldPeakRateEnabled: + v, ok := value.(bool) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPeakRateEnabled(v) + return nil + case group.FieldPeakStart: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPeakStart(v) + return nil + case group.FieldPeakEnd: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPeakEnd(v) + return nil + case group.FieldPeakRateMultiplier: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetPeakRateMultiplier(v) + return nil case group.FieldIsExclusive: v, ok := value.(bool) if !ok { @@ -17792,6 +18017,9 @@ func (m *GroupMutation) AddedFields() []string { if m.addrate_multiplier != nil { fields = append(fields, group.FieldRateMultiplier) } + if m.addpeak_rate_multiplier != nil { + fields = append(fields, group.FieldPeakRateMultiplier) + } if m.adddaily_limit_usd != nil { fields = append(fields, group.FieldDailyLimitUsd) } @@ -17838,6 +18066,8 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { switch name { case group.FieldRateMultiplier: return m.AddedRateMultiplier() + case group.FieldPeakRateMultiplier: + return m.AddedPeakRateMultiplier() case group.FieldDailyLimitUsd: return m.AddedDailyLimitUsd() case group.FieldWeeklyLimitUsd: @@ -17878,6 +18108,13 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error { } m.AddRateMultiplier(v) return nil + case group.FieldPeakRateMultiplier: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddPeakRateMultiplier(v) + return nil case group.FieldDailyLimitUsd: v, ok := value.(float64) if !ok { @@ -18076,6 +18313,18 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldRateMultiplier: m.ResetRateMultiplier() return nil + case group.FieldPeakRateEnabled: + m.ResetPeakRateEnabled() + return nil + case group.FieldPeakStart: + m.ResetPeakStart() + return nil + case group.FieldPeakEnd: + m.ResetPeakEnd() + return nil + case group.FieldPeakRateMultiplier: + m.ResetPeakRateMultiplier() + return nil case group.FieldIsExclusive: m.ResetIsExclusive() return nil diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 6a467e26b3..b86f6a0560 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -796,92 +796,112 @@ func init() { groupDescRateMultiplier := groupFields[2].Descriptor() // group.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field. group.DefaultRateMultiplier = groupDescRateMultiplier.Default.(float64) + // groupDescPeakRateEnabled is the schema descriptor for peak_rate_enabled field. + groupDescPeakRateEnabled := groupFields[3].Descriptor() + // group.DefaultPeakRateEnabled holds the default value on creation for the peak_rate_enabled field. + group.DefaultPeakRateEnabled = groupDescPeakRateEnabled.Default.(bool) + // groupDescPeakStart is the schema descriptor for peak_start field. + groupDescPeakStart := groupFields[4].Descriptor() + // group.DefaultPeakStart holds the default value on creation for the peak_start field. + group.DefaultPeakStart = groupDescPeakStart.Default.(string) + // group.PeakStartValidator is a validator for the "peak_start" field. It is called by the builders before save. + group.PeakStartValidator = groupDescPeakStart.Validators[0].(func(string) error) + // groupDescPeakEnd is the schema descriptor for peak_end field. + groupDescPeakEnd := groupFields[5].Descriptor() + // group.DefaultPeakEnd holds the default value on creation for the peak_end field. + group.DefaultPeakEnd = groupDescPeakEnd.Default.(string) + // group.PeakEndValidator is a validator for the "peak_end" field. It is called by the builders before save. + group.PeakEndValidator = groupDescPeakEnd.Validators[0].(func(string) error) + // groupDescPeakRateMultiplier is the schema descriptor for peak_rate_multiplier field. + groupDescPeakRateMultiplier := groupFields[6].Descriptor() + // group.DefaultPeakRateMultiplier holds the default value on creation for the peak_rate_multiplier field. + group.DefaultPeakRateMultiplier = groupDescPeakRateMultiplier.Default.(float64) // groupDescIsExclusive is the schema descriptor for is_exclusive field. - groupDescIsExclusive := groupFields[3].Descriptor() + groupDescIsExclusive := groupFields[7].Descriptor() // group.DefaultIsExclusive holds the default value on creation for the is_exclusive field. group.DefaultIsExclusive = groupDescIsExclusive.Default.(bool) // groupDescStatus is the schema descriptor for status field. - groupDescStatus := groupFields[4].Descriptor() + groupDescStatus := groupFields[8].Descriptor() // group.DefaultStatus holds the default value on creation for the status field. group.DefaultStatus = groupDescStatus.Default.(string) // group.StatusValidator is a validator for the "status" field. It is called by the builders before save. group.StatusValidator = groupDescStatus.Validators[0].(func(string) error) // groupDescPlatform is the schema descriptor for platform field. - groupDescPlatform := groupFields[5].Descriptor() + groupDescPlatform := groupFields[9].Descriptor() // group.DefaultPlatform holds the default value on creation for the platform field. group.DefaultPlatform = groupDescPlatform.Default.(string) // group.PlatformValidator is a validator for the "platform" field. It is called by the builders before save. group.PlatformValidator = groupDescPlatform.Validators[0].(func(string) error) // groupDescSubscriptionType is the schema descriptor for subscription_type field. - groupDescSubscriptionType := groupFields[6].Descriptor() + groupDescSubscriptionType := groupFields[10].Descriptor() // group.DefaultSubscriptionType holds the default value on creation for the subscription_type field. group.DefaultSubscriptionType = groupDescSubscriptionType.Default.(string) // group.SubscriptionTypeValidator is a validator for the "subscription_type" field. It is called by the builders before save. group.SubscriptionTypeValidator = groupDescSubscriptionType.Validators[0].(func(string) error) // groupDescDefaultValidityDays is the schema descriptor for default_validity_days field. - groupDescDefaultValidityDays := groupFields[10].Descriptor() + groupDescDefaultValidityDays := groupFields[14].Descriptor() // group.DefaultDefaultValidityDays holds the default value on creation for the default_validity_days field. group.DefaultDefaultValidityDays = groupDescDefaultValidityDays.Default.(int) // groupDescAllowImageGeneration is the schema descriptor for allow_image_generation field. - groupDescAllowImageGeneration := groupFields[11].Descriptor() + groupDescAllowImageGeneration := groupFields[15].Descriptor() // group.DefaultAllowImageGeneration holds the default value on creation for the allow_image_generation field. group.DefaultAllowImageGeneration = groupDescAllowImageGeneration.Default.(bool) // groupDescImageRateIndependent is the schema descriptor for image_rate_independent field. - groupDescImageRateIndependent := groupFields[12].Descriptor() + groupDescImageRateIndependent := groupFields[16].Descriptor() // group.DefaultImageRateIndependent holds the default value on creation for the image_rate_independent field. group.DefaultImageRateIndependent = groupDescImageRateIndependent.Default.(bool) // groupDescImageRateMultiplier is the schema descriptor for image_rate_multiplier field. - groupDescImageRateMultiplier := groupFields[13].Descriptor() + groupDescImageRateMultiplier := groupFields[17].Descriptor() // group.DefaultImageRateMultiplier holds the default value on creation for the image_rate_multiplier field. group.DefaultImageRateMultiplier = groupDescImageRateMultiplier.Default.(float64) // groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field. - groupDescClaudeCodeOnly := groupFields[17].Descriptor() + groupDescClaudeCodeOnly := groupFields[21].Descriptor() // group.DefaultClaudeCodeOnly holds the default value on creation for the claude_code_only field. group.DefaultClaudeCodeOnly = groupDescClaudeCodeOnly.Default.(bool) // groupDescModelRoutingEnabled is the schema descriptor for model_routing_enabled field. - groupDescModelRoutingEnabled := groupFields[21].Descriptor() + groupDescModelRoutingEnabled := groupFields[25].Descriptor() // group.DefaultModelRoutingEnabled holds the default value on creation for the model_routing_enabled field. group.DefaultModelRoutingEnabled = groupDescModelRoutingEnabled.Default.(bool) // groupDescMcpXMLInject is the schema descriptor for mcp_xml_inject field. - groupDescMcpXMLInject := groupFields[22].Descriptor() + groupDescMcpXMLInject := groupFields[26].Descriptor() // group.DefaultMcpXMLInject holds the default value on creation for the mcp_xml_inject field. group.DefaultMcpXMLInject = groupDescMcpXMLInject.Default.(bool) // groupDescSupportedModelScopes is the schema descriptor for supported_model_scopes field. - groupDescSupportedModelScopes := groupFields[23].Descriptor() + groupDescSupportedModelScopes := groupFields[27].Descriptor() // group.DefaultSupportedModelScopes holds the default value on creation for the supported_model_scopes field. group.DefaultSupportedModelScopes = groupDescSupportedModelScopes.Default.([]string) // groupDescSortOrder is the schema descriptor for sort_order field. - groupDescSortOrder := groupFields[24].Descriptor() + groupDescSortOrder := groupFields[28].Descriptor() // group.DefaultSortOrder holds the default value on creation for the sort_order field. group.DefaultSortOrder = groupDescSortOrder.Default.(int) // groupDescAllowMessagesDispatch is the schema descriptor for allow_messages_dispatch field. - groupDescAllowMessagesDispatch := groupFields[25].Descriptor() + groupDescAllowMessagesDispatch := groupFields[29].Descriptor() // group.DefaultAllowMessagesDispatch holds the default value on creation for the allow_messages_dispatch field. group.DefaultAllowMessagesDispatch = groupDescAllowMessagesDispatch.Default.(bool) // groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field. - groupDescRequireOauthOnly := groupFields[26].Descriptor() + groupDescRequireOauthOnly := groupFields[30].Descriptor() // group.DefaultRequireOauthOnly holds the default value on creation for the require_oauth_only field. group.DefaultRequireOauthOnly = groupDescRequireOauthOnly.Default.(bool) // groupDescRequirePrivacySet is the schema descriptor for require_privacy_set field. - groupDescRequirePrivacySet := groupFields[27].Descriptor() + groupDescRequirePrivacySet := groupFields[31].Descriptor() // group.DefaultRequirePrivacySet holds the default value on creation for the require_privacy_set field. group.DefaultRequirePrivacySet = groupDescRequirePrivacySet.Default.(bool) // groupDescDefaultMappedModel is the schema descriptor for default_mapped_model field. - groupDescDefaultMappedModel := groupFields[28].Descriptor() + groupDescDefaultMappedModel := groupFields[32].Descriptor() // group.DefaultDefaultMappedModel holds the default value on creation for the default_mapped_model field. group.DefaultDefaultMappedModel = groupDescDefaultMappedModel.Default.(string) // group.DefaultMappedModelValidator is a validator for the "default_mapped_model" field. It is called by the builders before save. group.DefaultMappedModelValidator = groupDescDefaultMappedModel.Validators[0].(func(string) error) // groupDescMessagesDispatchModelConfig is the schema descriptor for messages_dispatch_model_config field. - groupDescMessagesDispatchModelConfig := groupFields[29].Descriptor() + groupDescMessagesDispatchModelConfig := groupFields[33].Descriptor() // group.DefaultMessagesDispatchModelConfig holds the default value on creation for the messages_dispatch_model_config field. group.DefaultMessagesDispatchModelConfig = groupDescMessagesDispatchModelConfig.Default.(domain.OpenAIMessagesDispatchModelConfig) // groupDescModelsListConfig is the schema descriptor for models_list_config field. - groupDescModelsListConfig := groupFields[30].Descriptor() + groupDescModelsListConfig := groupFields[34].Descriptor() // group.DefaultModelsListConfig holds the default value on creation for the models_list_config field. group.DefaultModelsListConfig = groupDescModelsListConfig.Default.(domain.GroupModelsListConfig) // groupDescRpmLimit is the schema descriptor for rpm_limit field. - groupDescRpmLimit := groupFields[31].Descriptor() + groupDescRpmLimit := groupFields[35].Descriptor() // group.DefaultRpmLimit holds the default value on creation for the rpm_limit field. group.DefaultRpmLimit = groupDescRpmLimit.Default.(int) idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin() diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index 2a1715f8b2..306d088682 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -45,6 +45,22 @@ func (Group) Fields() []ent.Field { field.Float("rate_multiplier"). SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}). Default(1.0), + // 高峰时段倍率(added by migration 156) + field.Bool("peak_rate_enabled"). + Default(false). + Comment("是否启用高峰时段倍率"), + field.String("peak_start"). + MaxLen(5). + Default(""). + Comment("高峰开始时间 HH:MM(含),如 14:00;空表示未配置;不支持跨天"), + field.String("peak_end"). + MaxLen(5). + Default(""). + Comment("高峰结束时间 HH:MM(不含),必须大于 peak_start;不支持跨天,如 22:00-02:00"), + field.Float("peak_rate_multiplier"). + SchemaType(map[string]string{dialect.Postgres: "decimal(10,4)"}). + Default(1.0). + Comment("高峰时段叠加倍率,仅在 peak_rate_enabled 且处于 [peak_start, peak_end) 时乘入文本倍率"), field.Bool("is_exclusive"). Default(false), field.String("status"). diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 061611771d..9a1c2b87fe 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -71,6 +71,13 @@ func (f optionalLimitField) ToServiceInput() *float64 { return &zero } +func derefFloat64Default(p *float64, def float64) float64 { + if p != nil { + return *p + } + return def +} + // NewGroupHandler creates a new admin group handler func NewGroupHandler(adminService service.AdminService, dashboardService *service.DashboardService, groupCapacityService *service.GroupCapacityService) *GroupHandler { return &GroupHandler{ @@ -95,6 +102,10 @@ type CreateGroupRequest struct { AllowImageGeneration bool `json:"allow_image_generation"` ImageRateIndependent bool `json:"image_rate_independent"` ImageRateMultiplier *float64 `json:"image_rate_multiplier"` + PeakRateEnabled bool `json:"peak_rate_enabled"` + PeakStart string `json:"peak_start"` + PeakEnd string `json:"peak_end"` + PeakRateMultiplier *float64 `json:"peak_rate_multiplier"` ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice2K *float64 `json:"image_price_2k"` ImagePrice4K *float64 `json:"image_price_4k"` @@ -136,6 +147,10 @@ type UpdateGroupRequest struct { AllowImageGeneration *bool `json:"allow_image_generation"` ImageRateIndependent *bool `json:"image_rate_independent"` ImageRateMultiplier *float64 `json:"image_rate_multiplier"` + PeakRateEnabled *bool `json:"peak_rate_enabled"` + PeakStart *string `json:"peak_start"` + PeakEnd *string `json:"peak_end"` + PeakRateMultiplier *float64 `json:"peak_rate_multiplier"` ImagePrice1K *float64 `json:"image_price_1k"` ImagePrice2K *float64 `json:"image_price_2k"` ImagePrice4K *float64 `json:"image_price_4k"` @@ -277,6 +292,11 @@ func (h *GroupHandler) Create(c *gin.Context) { return } + if err := service.ValidatePeakRateConfig(req.SubscriptionType, req.PeakRateEnabled, req.PeakStart, req.PeakEnd, derefFloat64Default(req.PeakRateMultiplier, 1.0)); err != nil { + response.BadRequest(c, err.Error()) + return + } + group, err := h.adminService.CreateGroup(c.Request.Context(), &service.CreateGroupInput{ Name: req.Name, Description: req.Description, @@ -290,6 +310,10 @@ func (h *GroupHandler) Create(c *gin.Context) { AllowImageGeneration: req.AllowImageGeneration, ImageRateIndependent: req.ImageRateIndependent, ImageRateMultiplier: req.ImageRateMultiplier, + PeakRateEnabled: req.PeakRateEnabled, + PeakStart: req.PeakStart, + PeakEnd: req.PeakEnd, + PeakRateMultiplier: req.PeakRateMultiplier, ImagePrice1K: req.ImagePrice1K, ImagePrice2K: req.ImagePrice2K, ImagePrice4K: req.ImagePrice4K, @@ -346,6 +370,10 @@ func (h *GroupHandler) Update(c *gin.Context) { AllowImageGeneration: req.AllowImageGeneration, ImageRateIndependent: req.ImageRateIndependent, ImageRateMultiplier: req.ImageRateMultiplier, + PeakRateEnabled: req.PeakRateEnabled, + PeakStart: req.PeakStart, + PeakEnd: req.PeakEnd, + PeakRateMultiplier: req.PeakRateMultiplier, ImagePrice1K: req.ImagePrice1K, ImagePrice2K: req.ImagePrice2K, ImagePrice4K: req.ImagePrice4K, diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index 415d18dcaa..11d06331af 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -181,6 +181,10 @@ func groupFromServiceBase(g *service.Group) Group { AllowImageGeneration: g.AllowImageGeneration, ImageRateIndependent: g.ImageRateIndependent, ImageRateMultiplier: g.ImageRateMultiplier, + PeakRateEnabled: g.PeakRateEnabled, + PeakStart: g.PeakStart, + PeakEnd: g.PeakEnd, + PeakRateMultiplier: g.PeakRateMultiplier, ImagePrice1K: g.ImagePrice1K, ImagePrice2K: g.ImagePrice2K, ImagePrice4K: g.ImagePrice4K, diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index d8748baa34..1832b6e490 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -97,12 +97,17 @@ type Group struct { MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` // 图片生成计费配置(仅 antigravity 平台使用) - AllowImageGeneration bool `json:"allow_image_generation"` - ImageRateIndependent bool `json:"image_rate_independent"` - ImageRateMultiplier float64 `json:"image_rate_multiplier"` - ImagePrice1K *float64 `json:"image_price_1k"` - ImagePrice2K *float64 `json:"image_price_2k"` - ImagePrice4K *float64 `json:"image_price_4k"` + AllowImageGeneration bool `json:"allow_image_generation"` + ImageRateIndependent bool `json:"image_rate_independent"` + ImageRateMultiplier float64 `json:"image_rate_multiplier"` + // 高峰时段倍率配置 + PeakRateEnabled bool `json:"peak_rate_enabled"` + PeakStart string `json:"peak_start"` + PeakEnd string `json:"peak_end"` + PeakRateMultiplier float64 `json:"peak_rate_multiplier"` + ImagePrice1K *float64 `json:"image_price_1k"` + ImagePrice2K *float64 `json:"image_price_2k"` + ImagePrice4K *float64 `json:"image_price_4k"` // Claude Code 客户端限制 ClaudeCodeOnly bool `json:"claude_code_only"` diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index e5eb34db76..76cad809c3 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -194,6 +194,10 @@ func (r *apiKeyRepository) GetByKeyForAuth(ctx context.Context, key string) (*se group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldRpmLimit, + group.FieldPeakRateEnabled, + group.FieldPeakStart, + group.FieldPeakEnd, + group.FieldPeakRateMultiplier, ) }). Only(ctx) @@ -814,6 +818,10 @@ func groupEntityToService(g *dbent.Group) *service.Group { MessagesDispatchModelConfig: g.MessagesDispatchModelConfig, ModelsListConfig: g.ModelsListConfig, RPMLimit: g.RpmLimit, + PeakRateEnabled: g.PeakRateEnabled, + PeakStart: g.PeakStart, + PeakEnd: g.PeakEnd, + PeakRateMultiplier: g.PeakRateMultiplier, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, } diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index ac8669abdd..4e839b6a12 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -67,7 +67,11 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er SetDefaultMappedModel(groupIn.DefaultMappedModel). SetMessagesDispatchModelConfig(groupIn.MessagesDispatchModelConfig). SetModelsListConfig(groupIn.ModelsListConfig). - SetRpmLimit(groupIn.RPMLimit) + SetRpmLimit(groupIn.RPMLimit). + SetPeakRateEnabled(groupIn.PeakRateEnabled). + SetPeakStart(groupIn.PeakStart). + SetPeakEnd(groupIn.PeakEnd). + SetPeakRateMultiplier(groupIn.PeakRateMultiplier) // 设置模型路由配置 if groupIn.ModelRouting != nil { @@ -143,7 +147,11 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er SetDefaultMappedModel(groupIn.DefaultMappedModel). SetMessagesDispatchModelConfig(groupIn.MessagesDispatchModelConfig). SetModelsListConfig(groupIn.ModelsListConfig). - SetRpmLimit(groupIn.RPMLimit) + SetRpmLimit(groupIn.RPMLimit). + SetPeakRateEnabled(groupIn.PeakRateEnabled). + SetPeakStart(groupIn.PeakStart). + SetPeakEnd(groupIn.PeakEnd). + SetPeakRateMultiplier(groupIn.PeakRateMultiplier) // 显式处理可空字段:nil 需要 clear,非 nil 需要 set。 if groupIn.DailyLimitUSD != nil { diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 7a5637dc75..c38bc74529 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -209,11 +209,16 @@ type CreateGroupInput struct { AllowImageGeneration bool ImageRateIndependent bool ImageRateMultiplier *float64 - ImagePrice1K *float64 - ImagePrice2K *float64 - ImagePrice4K *float64 - ClaudeCodeOnly bool // 仅允许 Claude Code 客户端 - FallbackGroupID *int64 // 降级分组 ID + // 高峰时段倍率配置(PeakRateMultiplier 为 nil 时按 1.0 处理) + PeakRateEnabled bool + PeakStart string + PeakEnd string + PeakRateMultiplier *float64 + ImagePrice1K *float64 + ImagePrice2K *float64 + ImagePrice4K *float64 + ClaudeCodeOnly bool // 仅允许 Claude Code 客户端 + FallbackGroupID *int64 // 降级分组 ID // 无效请求兜底分组 ID(仅 anthropic 平台使用) FallbackGroupIDOnInvalidRequest *int64 // 模型路由配置(仅 anthropic 平台使用) @@ -250,11 +255,16 @@ type UpdateGroupInput struct { AllowImageGeneration *bool ImageRateIndependent *bool ImageRateMultiplier *float64 - ImagePrice1K *float64 - ImagePrice2K *float64 - ImagePrice4K *float64 - ClaudeCodeOnly *bool // 仅允许 Claude Code 客户端 - FallbackGroupID *int64 // 降级分组 ID + // 高峰时段倍率配置(nil 表示不修改) + PeakRateEnabled *bool + PeakStart *string + PeakEnd *string + PeakRateMultiplier *float64 + ImagePrice1K *float64 + ImagePrice2K *float64 + ImagePrice4K *float64 + ClaudeCodeOnly *bool // 仅允许 Claude Code 客户端 + FallbackGroupID *int64 // 降级分组 ID // 无效请求兜底分组 ID(仅 anthropic 平台使用) FallbackGroupIDOnInvalidRequest *int64 // 模型路由配置(仅 anthropic 平台使用) @@ -1824,6 +1834,14 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn imageRateMultiplier = *input.ImageRateMultiplier } + peakRateMultiplier := 1.0 + if input.PeakRateMultiplier != nil { + peakRateMultiplier = *input.PeakRateMultiplier + } + if err := ValidatePeakRateConfig(subscriptionType, input.PeakRateEnabled, input.PeakStart, input.PeakEnd, peakRateMultiplier); err != nil { + return nil, err + } + // 校验降级分组 if input.FallbackGroupID != nil { if err := s.validateFallbackGroup(ctx, 0, *input.FallbackGroupID); err != nil { @@ -1893,6 +1911,10 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn AllowImageGeneration: input.AllowImageGeneration, ImageRateIndependent: input.ImageRateIndependent, ImageRateMultiplier: imageRateMultiplier, + PeakRateEnabled: input.PeakRateEnabled, + PeakStart: input.PeakStart, + PeakEnd: input.PeakEnd, + PeakRateMultiplier: peakRateMultiplier, ImagePrice1K: imagePrice1K, ImagePrice2K: imagePrice2K, ImagePrice4K: imagePrice4K, @@ -2082,6 +2104,23 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd } group.ImageRateMultiplier = *input.ImageRateMultiplier } + if input.PeakRateEnabled != nil { + group.PeakRateEnabled = *input.PeakRateEnabled + } + if input.PeakStart != nil { + group.PeakStart = *input.PeakStart + } + if input.PeakEnd != nil { + group.PeakEnd = *input.PeakEnd + } + if input.PeakRateMultiplier != nil { + group.PeakRateMultiplier = *input.PeakRateMultiplier + } + // 收敛校验:Update 可能只传部分 peak 字段,需对合并后的最终配置统一校验, + // 防止单独修改 start/end 导致最终 start>=end 等非法配置入库。 + if err := ValidatePeakRateConfig(group.SubscriptionType, group.PeakRateEnabled, group.PeakStart, group.PeakEnd, group.PeakRateMultiplier); err != nil { + return nil, err + } if input.ImagePrice1K != nil { group.ImagePrice1K = normalizePrice(input.ImagePrice1K) } diff --git a/backend/internal/service/api_key_auth_cache.go b/backend/internal/service/api_key_auth_cache.go index 1b1f2d3b69..8089e90b97 100644 --- a/backend/internal/service/api_key_auth_cache.go +++ b/backend/internal/service/api_key_auth_cache.go @@ -93,6 +93,14 @@ type APIKeyAuthGroupSnapshot struct { // RPMLimit 分组级每分钟请求数上限(0 = 不限制);用于 billing_cache_service.checkRPM 级联判断。 RPMLimit int `json:"rpm_limit"` + + // 高峰时段倍率:PeakRateEnabled 为 true 且请求时刻处于 [PeakStart, PeakEnd) 时, + // 文本计费倍率额外乘以 PeakRateMultiplier(详见 Group.PeakMultiplierAt)。 + // 必须随快照缓存,否则扣费路径拿到的 apiKey.Group 缺字段、高峰倍率失效。 + PeakRateEnabled bool `json:"peak_rate_enabled"` + PeakStart string `json:"peak_start"` + PeakEnd string `json:"peak_end"` + PeakRateMultiplier float64 `json:"peak_rate_multiplier"` } // APIKeyAuthCacheEntry 缓存条目,支持负缓存 diff --git a/backend/internal/service/api_key_auth_cache_impl.go b/backend/internal/service/api_key_auth_cache_impl.go index 5ab557f6f9..b5aedf271e 100644 --- a/backend/internal/service/api_key_auth_cache_impl.go +++ b/backend/internal/service/api_key_auth_cache_impl.go @@ -14,7 +14,7 @@ import ( "github.com/dgraph-io/ristretto" ) -const apiKeyAuthSnapshotVersion = 12 // v12: include exclusive group authorization fields +const apiKeyAuthSnapshotVersion = 13 // v13: include group peak rate fields type apiKeyAuthCacheConfig struct { l1Size int @@ -276,6 +276,10 @@ func (s *APIKeyService) snapshotFromAPIKey(ctx context.Context, apiKey *APIKey) MessagesDispatchModelConfig: apiKey.Group.MessagesDispatchModelConfig, ModelsListConfig: apiKey.Group.ModelsListConfig, RPMLimit: apiKey.Group.RPMLimit, + PeakRateEnabled: apiKey.Group.PeakRateEnabled, + PeakStart: apiKey.Group.PeakStart, + PeakEnd: apiKey.Group.PeakEnd, + PeakRateMultiplier: apiKey.Group.PeakRateMultiplier, } } return snapshot @@ -349,6 +353,10 @@ func (s *APIKeyService) snapshotToAPIKey(key string, snapshot *APIKeyAuthSnapsho MessagesDispatchModelConfig: snapshot.Group.MessagesDispatchModelConfig, ModelsListConfig: snapshot.Group.ModelsListConfig, RPMLimit: snapshot.Group.RPMLimit, + PeakRateEnabled: snapshot.Group.PeakRateEnabled, + PeakStart: snapshot.Group.PeakStart, + PeakEnd: snapshot.Group.PeakEnd, + PeakRateMultiplier: snapshot.Group.PeakRateMultiplier, } } s.compileAPIKeyIPRules(apiKey) diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index d9f3ef93b6..4781da2435 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -30,6 +30,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" + "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/pkg/usagestats" "github.com/Wei-Shaw/sub2api/internal/util/responseheaders" "github.com/Wei-Shaw/sub2api/internal/util/urlvalidator" @@ -9436,7 +9437,9 @@ func (s *GatewayService) recordUsageCore(ctx context.Context, input *recordUsage groupDefault := apiKey.Group.RateMultiplier multiplier = s.getUserGroupRateMultiplier(ctx, user.ID, *apiKey.GroupID, groupDefault) } - imageMultiplier := resolveImageRateMultiplier(apiKey, multiplier) + // 文本倍率叠加高峰因子(仅文本,图片倍率不受影响)。高峰因子按请求时刻现算, + // 不并入上面的 getUserGroupRateMultiplier,以免污染 user:group 倍率缓存。 + multiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, multiplier, timezone.Now()) // 确定计费模型 billingModel := forwardResultBillingModel(result.Model, result.UpstreamModel) diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index 9aa2a52f4a..f1e7fcb283 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -1,10 +1,13 @@ package service import ( + "errors" + "fmt" "strings" "time" "github.com/Wei-Shaw/sub2api/internal/domain" + "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" ) type OpenAIMessagesDispatchModelConfig = domain.OpenAIMessagesDispatchModelConfig @@ -16,7 +19,13 @@ type Group struct { Description string Platform string RateMultiplier float64 - IsExclusive bool + // 高峰时段倍率:peak_rate_enabled 为 true 且当前时刻处于 [PeakStart, PeakEnd) 时, + // 文本计费倍率额外乘以 PeakRateMultiplier。详见 PeakMultiplierAt。 + PeakRateEnabled bool + PeakStart string + PeakEnd string + PeakRateMultiplier float64 + IsExclusive bool Status string Hydrated bool // indicates the group was loaded from a trusted repository source @@ -167,3 +176,78 @@ func matchModelPattern(pattern, model string) bool { return false } + +// parseMinutes 把 "HH:MM" 解析为当日分钟数(0..1439)。格式非法返回 (0,false)。 +func parseMinutes(hhmm string) (int, bool) { + t, err := time.Parse("15:04", hhmm) + if err != nil { + return 0, false + } + return t.Hour()*60 + t.Minute(), true +} + +// PeakMultiplierAt 返回指定时刻 now 的高峰因子。 +// - 未启用 / 未配置 / 配置非法(start>=end 或格式错误) / 非高峰时段 → 返回 1.0(安全降级) +// - 区间为左闭右开 [PeakStart, PeakEnd),仅支持当日区间,不支持跨天(如 22:00-次日02:00) +// - 时刻基于全局系统时区(timezone.Location)判定 +// 该方法是纯函数,不读取任何外部状态,便于单测。 +func (g *Group) PeakMultiplierAt(now time.Time) float64 { + if g == nil || !g.IsSubscriptionType() || !g.PeakRateEnabled || g.PeakStart == "" || g.PeakEnd == "" { + return 1.0 + } + start, ok1 := parseMinutes(g.PeakStart) + end, ok2 := parseMinutes(g.PeakEnd) + if !ok1 || !ok2 || start >= end { + return 1.0 + } + t := now.In(timezone.Location()) + cur := t.Hour()*60 + t.Minute() + if cur >= start && cur < end { + return g.PeakRateMultiplier + } + return 1.0 +} + +// ValidatePeakRateConfig 是高峰倍率配置的唯一校验来源,供 handler 与 service 层共用。 +// enabled=true 时仅允许订阅类型分组;并要求 start/end 合法且 end>start(不支持跨天),multiplier>=0。 +// enabled=false 时放行(不关心类型)。subscriptionType 为空按 standard 处理。 +func ValidatePeakRateConfig(subscriptionType string, enabled bool, start, end string, multiplier float64) error { + if !enabled { + return nil + } + if subscriptionType != SubscriptionTypeSubscription { + return errors.New("高峰时段倍率仅支持订阅类型分组") + } + if start == "" || end == "" { + return errors.New("peak_rate_enabled 为 true 时 peak_start 与 peak_end 必填") + } + st, err1 := time.Parse("15:04", start) + if err1 != nil { + return fmt.Errorf("peak_start 格式应为 HH:MM,got %q", start) + } + en, err2 := time.Parse("15:04", end) + if err2 != nil { + return fmt.Errorf("peak_end 格式应为 HH:MM,got %q", end) + } + if st.Hour()*60+st.Minute() >= en.Hour()*60+en.Minute() { + return errors.New("peak_end 必须大于 peak_start(不支持跨天区间,如 22:00-02:00)") + } + if multiplier < 0 { + return errors.New("peak_rate_multiplier 不能为负") + } + return nil +} + +// computePeakAwareMultipliers 把"基础文本倍率 base"(已含系统/分组/用户级倍率,但不含高峰) +// 拆分为最终文本倍率与图片倍率:图片倍率基于 base 现算、不受高峰影响;文本倍率在 base 上叠加高峰因子。 +// gateway_service.recordUsageCore 与 openai_gateway_service.RecordUsage 共用此函数, +// 锁死"高峰因子只乘入文本倍率、图片倍率不受影响"这一叠加顺序——任何调换都会被 group_peak_rate_test 覆盖。 +func computePeakAwareMultipliers(apiKey *APIKey, base float64, now time.Time) (text, image float64) { + image = resolveImageRateMultiplier(apiKey, base) + peak := 1.0 + if apiKey != nil && apiKey.Group != nil { + peak = apiKey.Group.PeakMultiplierAt(now) + } + text = base * peak + return +} diff --git a/backend/internal/service/group_peak_rate_test.go b/backend/internal/service/group_peak_rate_test.go new file mode 100644 index 0000000000..f63d298da6 --- /dev/null +++ b/backend/internal/service/group_peak_rate_test.go @@ -0,0 +1,238 @@ +package service + +import ( + "context" + "math" + "testing" + "time" + + "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" +) + +func init() { + // 测试固定全局时区为 UTC,确保判定可复现。 + _ = timezone.Init("UTC") +} + +func newPeakGroup(enabled bool, start, end string, mult float64) *Group { + return &Group{ + SubscriptionType: "subscription", + PeakRateEnabled: enabled, + PeakStart: start, + PeakEnd: end, + PeakRateMultiplier: mult, + } +} + +func at(hour, min int) time.Time { + return time.Date(2026, 6, 29, hour, min, 0, 0, time.UTC) +} + +func TestPeakMultiplierAt_DisabledOrUnconfigured(t *testing.T) { + cases := []struct { + name string + g *Group + }{ + {"disabled", newPeakGroup(false, "14:00", "18:00", 3.0)}, + {"empty start", newPeakGroup(true, "", "18:00", 3.0)}, + {"empty end", newPeakGroup(true, "14:00", "", 3.0)}, + {"invalid start>=end", newPeakGroup(true, "18:00", "14:00", 3.0)}, + {"equal start==end", newPeakGroup(true, "14:00", "14:00", 3.0)}, + {"malformed start", newPeakGroup(true, "99:99", "18:00", 3.0)}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + if got := c.g.PeakMultiplierAt(at(15, 0)); got != 1.0 { + t.Fatalf("expect 1.0, got %v", got) + } + }) + } +} + +func TestPeakMultiplierAt_NilReceiver(t *testing.T) { + var g *Group + if got := g.PeakMultiplierAt(at(15, 0)); got != 1.0 { + t.Fatalf("expect 1.0, got %v", got) + } +} + +func TestPeakMultiplierAt_Boundaries(t *testing.T) { + g := newPeakGroup(true, "14:00", "18:00", 3.0) + cases := []struct { + t time.Time + want float64 + }{ + {at(13, 59), 1.0}, + {at(14, 0), 3.0}, + {at(15, 30), 3.0}, + {at(17, 59), 3.0}, + {at(18, 0), 1.0}, + {at(23, 0), 1.0}, + } + for _, c := range cases { + t.Run(c.t.Format("15:04"), func(t *testing.T) { + if got := g.PeakMultiplierAt(c.t); got != c.want { + t.Fatalf("at %s: expect %v, got %v", c.t.Format("15:04"), c.want, got) + } + }) + } +} + +func TestPeakMultiplierAt_RespectsTimezoneLocation(t *testing.T) { + // 全局时区为 UTC。北京 15:00 = UTC 07:00,不在 [14:00,18:00)。 + nonUTC := time.Date(2026, 6, 29, 15, 0, 0, 0, mustLoad("Asia/Shanghai")) + g := newPeakGroup(true, "14:00", "18:00", 3.0) + if got := g.PeakMultiplierAt(nonUTC); got != 1.0 { + t.Fatalf("expect 1.0 (converted to UTC 07:00), got %v", got) + } +} + +func mustLoad(name string) *time.Location { + loc, err := time.LoadLocation(name) + if err != nil { + panic(err) + } + return loc +} + +func TestValidatePeakRateConfig(t *testing.T) { + cases := []struct { + name string + subType string + enabled bool + start string + end string + mult float64 + wantErr bool + }{ + {"disabled passes through", "subscription", false, "", "", 0, false}, + {"subscription enabled valid", "subscription", true, "14:00", "18:00", 3.0, false}, + {"standard enabled rejected", "standard", true, "14:00", "18:00", 3.0, true}, + {"empty type treated as standard", "", true, "14:00", "18:00", 3.0, true}, + {"standard disabled passes", "standard", false, "", "", 0, false}, + {"enabled empty start", "subscription", true, "", "18:00", 1.0, true}, + {"enabled empty end", "subscription", true, "14:00", "", 1.0, true}, + {"enabled malformed start", "subscription", true, "99:99", "18:00", 1.0, true}, + {"enabled malformed end", "subscription", true, "14:00", "25:00", 1.0, true}, + {"enabled equal start==end", "subscription", true, "14:00", "14:00", 1.0, true}, + {"enabled cross-day rejected", "subscription", true, "22:00", "02:00", 1.0, true}, + {"enabled negative multiplier", "subscription", true, "14:00", "18:00", -0.5, true}, + {"enabled zero multiplier allowed", "subscription", true, "14:00", "18:00", 0, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := ValidatePeakRateConfig(c.subType, c.enabled, c.start, c.end, c.mult) + if c.wantErr && err == nil { + t.Fatalf("expect error, got nil") + } + if !c.wantErr && err != nil { + t.Fatalf("expect no error, got %v", err) + } + }) + } +} + +func TestPeakMultiplierAt_StandardTypeDegradesToOne(t *testing.T) { + g := newPeakGroup(true, "14:00", "18:00", 3.0) + g.SubscriptionType = "standard" + if got := g.PeakMultiplierAt(at(15, 30)); got != 1.0 { + t.Fatalf("standard group must degrade to 1.0, got %v", got) + } + + sub := newPeakGroup(true, "14:00", "18:00", 3.0) + sub.SubscriptionType = "subscription" + if got := sub.PeakMultiplierAt(at(15, 30)); got != 3.0 { + t.Fatalf("subscription group peak multiplier: got %v, want 3.0", got) + } +} + +// TestPeakMultiplier_GatewayBillingSequence 调用 gateway_service.recordUsageCore 与 +// openai_gateway_service.RecordUsage 共用的 computePeakAwareMultipliers,验证计费叠加顺序: +// 图片倍率基于基础倍率算出且不受高峰影响,高峰因子只乘入文本倍率。 +// 若有人调换叠加顺序或把高峰并入 imageMultiplier,此测试会失败。 +func TestPeakMultiplier_GatewayBillingSequence(t *testing.T) { + const baseMultiplier = 0.8 + apiKey := &APIKey{Group: newPeakGroup(true, "14:00", "18:00", 3.0)} + approxEq := func(a, b float64) bool { return math.Abs(a-b) < 1e-9 } + + t.Run("peak hour amplifies text only", func(t *testing.T) { + now := at(15, 30) // 处于 [14:00, 18:00) + textMultiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, baseMultiplier, now) + if !approxEq(imageMultiplier, baseMultiplier) { + t.Fatalf("image multiplier must not be affected by peak: got %v, want %v", imageMultiplier, baseMultiplier) + } + if want := baseMultiplier * 3.0; !approxEq(textMultiplier, want) { + t.Fatalf("text multiplier should include peak factor: got %v, want %v", textMultiplier, want) + } + }) + + t.Run("off-peak leaves both multipliers at base", func(t *testing.T) { + now := at(20, 0) + textMultiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, baseMultiplier, now) + if !approxEq(imageMultiplier, baseMultiplier) { + t.Fatalf("image multiplier: got %v, want %v", imageMultiplier, baseMultiplier) + } + if !approxEq(textMultiplier, baseMultiplier) { + t.Fatalf("text multiplier should equal base off-peak: got %v, want %v", textMultiplier, baseMultiplier) + } + }) + + t.Run("image independent mode decoupled from peak", func(t *testing.T) { + indGroup := newPeakGroup(true, "14:00", "18:00", 3.0) + indGroup.ImageRateIndependent = true + indGroup.ImageRateMultiplier = 0.5 + indKey := &APIKey{Group: indGroup} + now := at(15, 30) + textMultiplier, imageMultiplier := computePeakAwareMultipliers(indKey, baseMultiplier, now) + if !approxEq(imageMultiplier, 0.5) { + t.Fatalf("independent image multiplier: got %v, want 0.5", imageMultiplier) + } + if want := baseMultiplier * 3.0; !approxEq(textMultiplier, want) { + t.Fatalf("text multiplier should include peak factor: got %v, want %v", textMultiplier, want) + } + }) + + t.Run("nil api key degrades to base multipliers", func(t *testing.T) { + now := at(15, 30) + textMultiplier, imageMultiplier := computePeakAwareMultipliers(nil, baseMultiplier, now) + if !approxEq(textMultiplier, baseMultiplier) { + t.Fatalf("nil group text multiplier: got %v, want %v", textMultiplier, baseMultiplier) + } + if !approxEq(imageMultiplier, baseMultiplier) { + t.Fatalf("nil group image multiplier: got %v, want %v", imageMultiplier, baseMultiplier) + } + }) +} + +// TestPeakMultiplier_SnapshotRoundTrip 防回归:认证缓存快照(APIKeyAuthGroupSnapshot) +// 必须携带高峰倍率 4 字段,否则扣费路径拿到的 apiKey.Group 会缺字段、PeakMultiplierAt 恒降级为 1.0。 +// 调用真实链路 snapshotFromAPIKey → snapshotToAPIKey,验证 peak 配置经快照往返后仍生效。 +func TestPeakMultiplier_SnapshotRoundTrip(t *testing.T) { + apiKey := &APIKey{ + User: &User{ID: 1, Status: StatusActive, Role: RoleUser}, + Group: newPeakGroup(true, "14:00", "18:00", 3.0), + } + svc := &APIKeyService{} + + snapshot := svc.snapshotFromAPIKey(context.Background(), apiKey) + if snapshot == nil || snapshot.Group == nil { + t.Fatalf("snapshot or snapshot.Group must not be nil") + } + restored := svc.snapshotToAPIKey("k", snapshot) + if restored.Group == nil { + t.Fatalf("restored.Group must not be nil") + } + + if !restored.Group.PeakRateEnabled || + restored.Group.PeakStart != "14:00" || + restored.Group.PeakEnd != "18:00" || + restored.Group.PeakRateMultiplier != 3.0 { + t.Fatalf("peak fields lost in snapshot round-trip: %+v", restored.Group) + } + if got := restored.Group.PeakMultiplierAt(at(15, 30)); got != 3.0 { + t.Fatalf("peak hour multiplier after round-trip: got %v, want 3.0", got) + } + if got := restored.Group.PeakMultiplierAt(at(20, 0)); got != 1.0 { + t.Fatalf("off-peak multiplier after round-trip: got %v, want 1.0", got) + } +} diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go index af0062db75..8a15438a43 100644 --- a/backend/internal/service/openai_gateway_service.go +++ b/backend/internal/service/openai_gateway_service.go @@ -24,6 +24,7 @@ import ( "github.com/Wei-Shaw/sub2api/internal/pkg/apicompat" "github.com/Wei-Shaw/sub2api/internal/pkg/ip" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" + "github.com/Wei-Shaw/sub2api/internal/pkg/timezone" "github.com/Wei-Shaw/sub2api/internal/pkg/openai" "github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat" "github.com/Wei-Shaw/sub2api/internal/pkg/xai" @@ -6283,7 +6284,9 @@ func (s *OpenAIGatewayService) RecordUsage(ctx context.Context, input *OpenAIRec } multiplier = resolver.Resolve(ctx, user.ID, *apiKey.GroupID, apiKey.Group.RateMultiplier) } - imageMultiplier := resolveImageRateMultiplier(apiKey, multiplier) + // 文本倍率叠加高峰因子(仅文本,图片倍率不受影响)。高峰因子按请求时刻现算, + // 不并入上面的 Resolve,以免污染 user:group 倍率缓存。 + multiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, multiplier, timezone.Now()) var cost *CostBreakdown var err error diff --git a/backend/migrations/158_add_group_peak_rate_multiplier.sql b/backend/migrations/158_add_group_peak_rate_multiplier.sql new file mode 100644 index 0000000000..f9924de751 --- /dev/null +++ b/backend/migrations/158_add_group_peak_rate_multiplier.sql @@ -0,0 +1,4 @@ +ALTER TABLE groups ADD COLUMN IF NOT EXISTS peak_rate_enabled BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS peak_start VARCHAR(5) NOT NULL DEFAULT ''; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS peak_end VARCHAR(5) NOT NULL DEFAULT ''; +ALTER TABLE groups ADD COLUMN IF NOT EXISTS peak_rate_multiplier DECIMAL(10,4) NOT NULL DEFAULT 1.0; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 4c5e58f777..790beee8d3 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -518,6 +518,11 @@ export interface Group { image_price_1k: number | null image_price_2k: number | null image_price_4k: number | null + // 高峰时段倍率配置 + peak_rate_enabled: boolean + peak_start: string + peak_end: string + peak_rate_multiplier: number // Claude Code 客户端限制 claude_code_only: boolean fallback_group_id: number | null @@ -636,6 +641,10 @@ export interface CreateGroupRequest { image_price_1k?: number | null image_price_2k?: number | null image_price_4k?: number | null + peak_rate_enabled?: boolean + peak_start?: string + peak_end?: string + peak_rate_multiplier?: number claude_code_only?: boolean fallback_group_id?: number | null fallback_group_id_on_invalid_request?: number | null @@ -671,6 +680,10 @@ export interface UpdateGroupRequest { image_price_1k?: number | null image_price_2k?: number | null image_price_4k?: number | null + peak_rate_enabled?: boolean + peak_start?: string + peak_end?: string + peak_rate_multiplier?: number claude_code_only?: boolean fallback_group_id?: number | null fallback_group_id_on_invalid_request?: number | null diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 4fa1596eed..8c584f80b3 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -853,6 +853,52 @@ + +