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 9a55ac6415..c771f9572d 100644
--- a/backend/ent/migrate/schema.go
+++ b/backend/ent/migrate/schema.go
@@ -657,6 +657,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"},
@@ -696,22 +700,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",
@@ -721,7 +725,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 4245a6749b..c71850e4ce 100644
--- a/backend/ent/mutation.go
+++ b/backend/ent/mutation.go
@@ -15325,6 +15325,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
@@ -15751,6 +15756,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
@@ -17533,7 +17702,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)
}
@@ -17552,6 +17721,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)
}
@@ -17659,6 +17840,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:
@@ -17738,6 +17927,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:
@@ -17847,6 +18044,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 {
@@ -18061,6 +18286,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)
}
@@ -18107,6 +18335,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:
@@ -18147,6 +18377,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 {
@@ -18345,6 +18582,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..2b8420db6d 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 158)
+ 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/available_channel_handler.go b/backend/internal/handler/available_channel_handler.go
index 8982b80def..8da4c07973 100644
--- a/backend/internal/handler/available_channel_handler.go
+++ b/backend/internal/handler/available_channel_handler.go
@@ -50,15 +50,19 @@ func (h *AvailableChannelHandler) featureEnabled(c *gin.Context) bool {
// userAvailableGroup 用户可见的分组概要(白名单字段)。
//
// 前端据此区分专属 vs 公开分组(IsExclusive)、订阅 vs 标准分组(SubscriptionType,
-// 订阅视觉加深),并用 RateMultiplier 作为默认倍率;用户专属倍率前端走
+// 订阅视觉加深),并展示默认倍率与高峰倍率规则;用户专属倍率前端走
// /groups/rates,和 API 密钥页面保持一致。
type userAvailableGroup struct {
- ID int64 `json:"id"`
- Name string `json:"name"`
- Platform string `json:"platform"`
- SubscriptionType string `json:"subscription_type"`
- RateMultiplier float64 `json:"rate_multiplier"`
- IsExclusive bool `json:"is_exclusive"`
+ ID int64 `json:"id"`
+ Name string `json:"name"`
+ Platform string `json:"platform"`
+ SubscriptionType string `json:"subscription_type"`
+ RateMultiplier float64 `json:"rate_multiplier"`
+ PeakRateEnabled bool `json:"peak_rate_enabled"`
+ PeakStart string `json:"peak_start"`
+ PeakEnd string `json:"peak_end"`
+ PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
+ IsExclusive bool `json:"is_exclusive"`
}
// userSupportedModelPricing 用户可见的定价字段白名单。
@@ -213,12 +217,16 @@ func filterUserVisibleGroups(
continue
}
visible = append(visible, userAvailableGroup{
- ID: g.ID,
- Name: g.Name,
- Platform: g.Platform,
- SubscriptionType: g.SubscriptionType,
- RateMultiplier: g.RateMultiplier,
- IsExclusive: g.IsExclusive,
+ ID: g.ID,
+ Name: g.Name,
+ Platform: g.Platform,
+ SubscriptionType: g.SubscriptionType,
+ RateMultiplier: g.RateMultiplier,
+ PeakRateEnabled: g.PeakRateEnabled,
+ PeakStart: g.PeakStart,
+ PeakEnd: g.PeakEnd,
+ PeakRateMultiplier: g.PeakRateMultiplier,
+ IsExclusive: g.IsExclusive,
})
}
return visible
diff --git a/backend/internal/handler/available_channel_handler_test.go b/backend/internal/handler/available_channel_handler_test.go
index 0a7ce6c466..bb53297980 100644
--- a/backend/internal/handler/available_channel_handler_test.go
+++ b/backend/internal/handler/available_channel_handler_test.go
@@ -101,13 +101,13 @@ func TestUserAvailableChannel_FieldWhitelist(t *testing.T) {
require.Truef(t, exists, "platform section must expose %q", key)
}
- // Group DTO 暴露区分专属/公开、订阅类型、默认倍率所需的字段,
+ // Group DTO 暴露区分专属/公开、订阅类型、默认倍率和高峰倍率规则所需的字段,
// 前端据此渲染 GroupBadge 并与 API 密钥页保持一致的视觉。
rawGroup, err := json.Marshal(row.Platforms[0].Groups[0])
require.NoError(t, err)
var groupDecoded map[string]any
require.NoError(t, json.Unmarshal(rawGroup, &groupDecoded))
- for _, key := range []string{"id", "name", "platform", "subscription_type", "rate_multiplier", "is_exclusive"} {
+ for _, key := range []string{"id", "name", "platform", "subscription_type", "rate_multiplier", "peak_rate_enabled", "peak_start", "peak_end", "peak_rate_multiplier", "is_exclusive"} {
_, exists := groupDecoded[key]
require.Truef(t, exists, "group DTO must expose %q", key)
}
diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go
index 72679de37b..5bbab4d45f 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 cef84465ef..b08dea5680 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/handler/payment_handler.go b/backend/internal/handler/payment_handler.go
index 4047cbfd27..7cdf73cd3d 100644
--- a/backend/internal/handler/payment_handler.go
+++ b/backend/internal/handler/payment_handler.go
@@ -54,25 +54,35 @@ func (h *PaymentHandler) GetPlans(c *gin.Context) {
}
// Enrich plans with group platform for frontend color coding
type planWithPlatform struct {
- ID int64 `json:"id"`
- GroupID int64 `json:"group_id"`
- GroupPlatform string `json:"group_platform"`
- Name string `json:"name"`
- Description string `json:"description"`
- Price float64 `json:"price"`
- OriginalPrice *float64 `json:"original_price,omitempty"`
- ValidityDays int `json:"validity_days"`
- ValidityUnit string `json:"validity_unit"`
- Features string `json:"features"`
- ProductName string `json:"product_name"`
- ForSale bool `json:"for_sale"`
- SortOrder int `json:"sort_order"`
+ ID int64 `json:"id"`
+ GroupID int64 `json:"group_id"`
+ GroupPlatform string `json:"group_platform"`
+ GroupName string `json:"group_name"`
+ RateMultiplier float64 `json:"rate_multiplier"`
+ PeakRateEnabled bool `json:"peak_rate_enabled"`
+ PeakStart string `json:"peak_start"`
+ PeakEnd string `json:"peak_end"`
+ PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Price float64 `json:"price"`
+ OriginalPrice *float64 `json:"original_price,omitempty"`
+ ValidityDays int `json:"validity_days"`
+ ValidityUnit string `json:"validity_unit"`
+ Features string `json:"features"`
+ ProductName string `json:"product_name"`
+ ForSale bool `json:"for_sale"`
+ SortOrder int `json:"sort_order"`
}
- platformMap := h.configService.GetGroupPlatformMap(c.Request.Context(), plans)
+ groupInfo := h.configService.GetGroupInfoMap(c.Request.Context(), plans)
result := make([]planWithPlatform, 0, len(plans))
for _, p := range plans {
+ gi := groupInfo[p.GroupID]
result = append(result, planWithPlatform{
- ID: int64(p.ID), GroupID: p.GroupID, GroupPlatform: platformMap[p.GroupID],
+ ID: int64(p.ID), GroupID: p.GroupID,
+ GroupPlatform: gi.Platform, GroupName: gi.Name,
+ RateMultiplier: gi.RateMultiplier, PeakRateEnabled: gi.PeakRateEnabled,
+ PeakStart: gi.PeakStart, PeakEnd: gi.PeakEnd, PeakRateMultiplier: gi.PeakRateMultiplier,
Name: p.Name, Description: p.Description, Price: p.Price, OriginalPrice: p.OriginalPrice,
ValidityDays: p.ValidityDays, ValidityUnit: p.ValidityUnit, Features: p.Features,
ProductName: p.ProductName, ForSale: p.ForSale, SortOrder: p.SortOrder,
@@ -121,7 +131,10 @@ func (h *PaymentHandler) GetCheckoutInfo(c *gin.Context) {
planList = append(planList, checkoutPlan{
ID: int64(p.ID), GroupID: p.GroupID,
GroupPlatform: gi.Platform, GroupName: gi.Name,
- RateMultiplier: gi.RateMultiplier, DailyLimitUSD: gi.DailyLimitUSD,
+ RateMultiplier: gi.RateMultiplier,
+ PeakRateEnabled: gi.PeakRateEnabled, PeakStart: gi.PeakStart,
+ PeakEnd: gi.PeakEnd, PeakRateMultiplier: gi.PeakRateMultiplier,
+ DailyLimitUSD: gi.DailyLimitUSD,
WeeklyLimitUSD: gi.WeeklyLimitUSD, MonthlyLimitUSD: gi.MonthlyLimitUSD,
ModelScopes: gi.ModelScopes,
Name: p.Name, Description: p.Description, Price: p.Price, OriginalPrice: p.OriginalPrice,
@@ -160,23 +173,27 @@ type checkoutInfoResponse struct {
}
type checkoutPlan struct {
- ID int64 `json:"id"`
- GroupID int64 `json:"group_id"`
- GroupPlatform string `json:"group_platform"`
- GroupName string `json:"group_name"`
- RateMultiplier float64 `json:"rate_multiplier"`
- DailyLimitUSD *float64 `json:"daily_limit_usd"`
- WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
- MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
- ModelScopes []string `json:"supported_model_scopes"`
- Name string `json:"name"`
- Description string `json:"description"`
- Price float64 `json:"price"`
- OriginalPrice *float64 `json:"original_price,omitempty"`
- ValidityDays int `json:"validity_days"`
- ValidityUnit string `json:"validity_unit"`
- Features []string `json:"features"`
- ProductName string `json:"product_name"`
+ ID int64 `json:"id"`
+ GroupID int64 `json:"group_id"`
+ GroupPlatform string `json:"group_platform"`
+ GroupName string `json:"group_name"`
+ RateMultiplier float64 `json:"rate_multiplier"`
+ PeakRateEnabled bool `json:"peak_rate_enabled"`
+ PeakStart string `json:"peak_start"`
+ PeakEnd string `json:"peak_end"`
+ PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
+ DailyLimitUSD *float64 `json:"daily_limit_usd"`
+ WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
+ MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
+ ModelScopes []string `json:"supported_model_scopes"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Price float64 `json:"price"`
+ OriginalPrice *float64 `json:"original_price,omitempty"`
+ ValidityDays int `json:"validity_days"`
+ ValidityUnit string `json:"validity_unit"`
+ Features []string `json:"features"`
+ ProductName string `json:"product_name"`
}
// parseFeatures splits a newline-separated features string into a string slice.
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/server/api_contract_test.go b/backend/internal/server/api_contract_test.go
index 533e0b404a..7b45b27895 100644
--- a/backend/internal/server/api_contract_test.go
+++ b/backend/internal/server/api_contract_test.go
@@ -317,6 +317,7 @@ func TestAPIContracts(t *testing.T) {
Description: "desc",
Platform: service.PlatformAnthropic,
RateMultiplier: 1.5,
+ PeakRateMultiplier: 1.0,
IsExclusive: false,
Status: service.StatusActive,
SubscriptionType: service.SubscriptionTypeStandard,
@@ -344,6 +345,10 @@ func TestAPIContracts(t *testing.T) {
"description": "desc",
"platform": "anthropic",
"rate_multiplier": 1.5,
+ "peak_rate_enabled": false,
+ "peak_start": "",
+ "peak_end": "",
+ "peak_rate_multiplier": 1,
"is_exclusive": false,
"status": "active",
"subscription_type": "standard",
diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go
index 8e38080c68..2a3189f61f 100644
--- a/backend/internal/service/admin_service.go
+++ b/backend/internal/service/admin_service.go
@@ -212,11 +212,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 平台使用)
@@ -253,11 +258,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 平台使用)
@@ -1836,6 +1846,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 {
@@ -1905,6 +1923,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,
@@ -2094,6 +2116,29 @@ 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
+ }
+ if group.SubscriptionType != SubscriptionTypeSubscription {
+ group.PeakRateEnabled = false
+ group.PeakStart = ""
+ group.PeakEnd = ""
+ group.PeakRateMultiplier = 1.0
+ }
+ // 收敛校验: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/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go
index eb3eff7f69..2951e13c53 100644
--- a/backend/internal/service/admin_service_group_test.go
+++ b/backend/internal/service/admin_service_group_test.go
@@ -375,6 +375,34 @@ func TestAdminService_UpdateGroup_InvalidatesAuthCacheOnRPMLimitChange(t *testin
require.Equal(t, []int64{1}, invalidator.groupIDs, "分组 RPMLimit 写入 auth snapshot,变更后必须失效 API Key 认证缓存")
}
+func TestAdminService_UpdateGroup_ClearsPeakRateWhenChangingToStandard(t *testing.T) {
+ existingGroup := &Group{
+ ID: 1,
+ Name: "existing-group",
+ Platform: PlatformOpenAI,
+ Status: StatusActive,
+ SubscriptionType: SubscriptionTypeSubscription,
+ PeakRateEnabled: true,
+ PeakStart: "14:00",
+ PeakEnd: "18:00",
+ PeakRateMultiplier: 3,
+ }
+ repo := &groupRepoStubForAdmin{getByID: existingGroup}
+ svc := &adminServiceImpl{groupRepo: repo}
+
+ group, err := svc.UpdateGroup(context.Background(), 1, &UpdateGroupInput{
+ SubscriptionType: SubscriptionTypeStandard,
+ })
+ require.NoError(t, err)
+ require.NotNil(t, group)
+ require.NotNil(t, repo.updated)
+ require.Equal(t, SubscriptionTypeStandard, repo.updated.SubscriptionType)
+ require.False(t, repo.updated.PeakRateEnabled)
+ require.Equal(t, "", repo.updated.PeakStart)
+ require.Equal(t, "", repo.updated.PeakEnd)
+ require.Equal(t, 1.0, repo.updated.PeakRateMultiplier)
+}
+
func TestAdminService_CreateGroup_NormalizesMessagesDispatchModelConfig(t *testing.T) {
repo := &groupRepoStubForAdmin{}
svc := &adminServiceImpl{groupRepo: repo}
diff --git a/backend/internal/service/api_key_auth_cache.go b/backend/internal/service/api_key_auth_cache.go
index 1b1f2d3b69..32c3910c9d 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) 时,
+ // token 计费倍率额外乘以 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/billing_service.go b/backend/internal/service/billing_service.go
index a781936598..891b46c878 100644
--- a/backend/internal/service/billing_service.go
+++ b/backend/internal/service/billing_service.go
@@ -1080,15 +1080,19 @@ func (s *BillingService) CalculateCostWithConfig(model string, tokens UsageToken
// 拆分为:范围内 (200k, 0) + 范围外 (10k, 10k)
// 范围内正常计费,范围外 × 2 计费
func (s *BillingService) CalculateCostWithLongContext(model string, tokens UsageTokens, rateMultiplier float64, threshold int, extraMultiplier float64) (*CostBreakdown, error) {
+ return s.calculateCostWithLongContext(model, tokens, rateMultiplier, threshold, extraMultiplier)
+}
+
+func (s *BillingService) calculateCostWithLongContext(model string, tokens UsageTokens, rateMultiplier float64, threshold int, extraMultiplier float64) (*CostBreakdown, error) {
// 未启用长上下文计费,直接走正常计费
if threshold <= 0 || extraMultiplier <= 1 {
- return s.CalculateCost(model, tokens, rateMultiplier)
+ return s.calculateCostInternal(model, tokens, rateMultiplier, "", nil)
}
// 计算总输入 token(缓存读取 + 新输入)
total := tokens.CacheReadTokens + tokens.InputTokens
if total <= threshold {
- return s.CalculateCost(model, tokens, rateMultiplier)
+ return s.calculateCostInternal(model, tokens, rateMultiplier, "", nil)
}
// 拆分成范围内和范围外
@@ -1119,7 +1123,7 @@ func (s *BillingService) CalculateCostWithLongContext(model string, tokens Usage
CacheCreation1hTokens: tokens.CacheCreation1hTokens,
ImageOutputTokens: tokens.ImageOutputTokens,
}
- inRangeCost, err := s.CalculateCost(model, inRangeTokens, rateMultiplier)
+ inRangeCost, err := s.calculateCostInternal(model, inRangeTokens, rateMultiplier, "", nil)
if err != nil {
return nil, err
}
@@ -1129,7 +1133,7 @@ func (s *BillingService) CalculateCostWithLongContext(model string, tokens Usage
InputTokens: outRangeInputTokens,
CacheReadTokens: outRangeCacheTokens,
}
- outRangeCost, err := s.CalculateCost(model, outRangeTokens, rateMultiplier*extraMultiplier)
+ outRangeCost, err := s.calculateCostInternal(model, outRangeTokens, rateMultiplier*extraMultiplier, "", nil)
if err != nil {
return inRangeCost, fmt.Errorf("out-range cost: %w", err)
}
diff --git a/backend/internal/service/billing_service_unified_test.go b/backend/internal/service/billing_service_unified_test.go
index e6a92d1a8c..eabbab3dfc 100644
--- a/backend/internal/service/billing_service_unified_test.go
+++ b/backend/internal/service/billing_service_unified_test.go
@@ -60,6 +60,28 @@ func TestCalculateCostUnified_TokenMode(t *testing.T) {
require.Equal(t, string(BillingModeToken), cost.BillingMode)
}
+func TestCalculateCostUnified_TokenModeAppliesRateMultiplierToImageTokens(t *testing.T) {
+ bs := newTestBillingService()
+ resolver := NewModelPricingResolver(nil, bs)
+
+ tokens := UsageTokens{InputTokens: 1000, OutputTokens: 600, ImageOutputTokens: 100}
+ cost, err := bs.CalculateCostUnified(CostInput{
+ Ctx: context.Background(),
+ Model: "claude-sonnet-4",
+ Tokens: tokens,
+ RateMultiplier: 3.0,
+ Resolver: resolver,
+ })
+ require.NoError(t, err)
+
+ textInput := 1000 * 3e-6
+ textOutput := 500 * 15e-6
+ imageOutput := 100 * 15e-6
+ require.InDelta(t, textInput+textOutput+imageOutput, cost.TotalCost, 1e-10)
+ require.InDelta(t, (textInput+textOutput+imageOutput)*3.0, cost.ActualCost, 1e-10)
+ require.InDelta(t, imageOutput, cost.ImageOutputCost, 1e-10)
+}
+
func TestCalculateCostUnified_PerRequestMode(t *testing.T) {
// Set up a ChannelService with a per-request pricing channel
cs := newTestChannelServiceWithCache(t, &channelCache{
diff --git a/backend/internal/service/channel_available.go b/backend/internal/service/channel_available.go
index d2d24659a1..eeaf7dac2e 100644
--- a/backend/internal/service/channel_available.go
+++ b/backend/internal/service/channel_available.go
@@ -10,15 +10,19 @@ import (
// AvailableGroupRef 渠道视图中关联分组的简要信息。
//
// 用户侧「可用渠道」页面据此展示:专属分组 vs 公开分组(IsExclusive)、
-// 订阅 vs 标准(SubscriptionType)、默认倍率(RateMultiplier)。用户专属倍率
-// 不在这里暴露,前端自己通过 /groups/rates 拉取,和 API 密钥页面保持一致。
+// 订阅 vs 标准(SubscriptionType)、默认倍率(RateMultiplier)与高峰倍率规则。
+// 用户专属倍率不在这里暴露,前端自己通过 /groups/rates 拉取,和 API 密钥页面保持一致。
type AvailableGroupRef struct {
- ID int64
- Name string
- Platform string
- SubscriptionType string
- RateMultiplier float64
- IsExclusive bool
+ ID int64
+ Name string
+ Platform string
+ SubscriptionType string
+ RateMultiplier float64
+ PeakRateEnabled bool
+ PeakStart string
+ PeakEnd string
+ PeakRateMultiplier float64
+ IsExclusive bool
}
// AvailableChannel 可用渠道视图:用于「可用渠道」页面展示渠道基础信息 +
@@ -59,12 +63,16 @@ func (s *ChannelService) ListAvailable(ctx context.Context) ([]AvailableChannel,
for i := range groups {
g := groups[i]
groupByID[g.ID] = AvailableGroupRef{
- ID: g.ID,
- Name: g.Name,
- Platform: g.Platform,
- SubscriptionType: g.SubscriptionType,
- RateMultiplier: g.RateMultiplier,
- IsExclusive: g.IsExclusive,
+ ID: g.ID,
+ Name: g.Name,
+ Platform: g.Platform,
+ SubscriptionType: g.SubscriptionType,
+ RateMultiplier: g.RateMultiplier,
+ PeakRateEnabled: g.PeakRateEnabled,
+ PeakStart: g.PeakStart,
+ PeakEnd: g.PeakEnd,
+ PeakRateMultiplier: g.PeakRateMultiplier,
+ IsExclusive: g.IsExclusive,
}
}
diff --git a/backend/internal/service/gateway_record_usage_test.go b/backend/internal/service/gateway_record_usage_test.go
index f8d98e55cd..c819eeca6e 100644
--- a/backend/internal/service/gateway_record_usage_test.go
+++ b/backend/internal/service/gateway_record_usage_test.go
@@ -233,6 +233,59 @@ func TestGatewayServiceRecordUsage_EmptyImageSizeDefaultsBeforeBillingAndPersist
require.InDelta(t, 0.19, usageRepo.lastLog.ActualCost, 1e-12)
}
+func TestGatewayServiceRecordUsage_PeakRateAffectsTokenModeImageOutputTokens(t *testing.T) {
+ groupID := int64(902)
+ usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
+ userRepo := &openAIRecordUsageUserRepoStub{}
+ svc := newGatewayRecordUsageServiceForTest(usageRepo, userRepo, &openAIRecordUsageSubRepoStub{})
+ svc.resolver = newOpenAITokenImageChannelPricingResolverForTest(t, groupID, "gemini-image")
+
+ err := svc.RecordUsage(context.Background(), &RecordUsageInput{
+ Result: &ForwardResult{
+ RequestID: "gateway_peak_image_tokens",
+ Model: "gemini-image",
+ ImageCount: 1,
+ Usage: ClaudeUsage{
+ InputTokens: 1000,
+ OutputTokens: 600,
+ ImageOutputTokens: 100,
+ },
+ Duration: time.Second,
+ },
+ APIKey: &APIKey{
+ ID: 802,
+ GroupID: i64p(groupID),
+ Group: &Group{
+ ID: groupID,
+ RateMultiplier: 1.0,
+ SubscriptionType: SubscriptionTypeSubscription,
+ PeakRateEnabled: true,
+ PeakStart: "00:00",
+ PeakEnd: "23:59",
+ PeakRateMultiplier: 3.0,
+ },
+ },
+ User: &User{ID: 602},
+ Account: &Account{ID: 702},
+ })
+
+ require.NoError(t, err)
+ require.NotNil(t, usageRepo.lastLog)
+ require.NotNil(t, usageRepo.lastLog.BillingMode)
+ require.Equal(t, string(BillingModeToken), *usageRepo.lastLog.BillingMode)
+ require.Equal(t, 3.0, usageRepo.lastLog.RateMultiplier)
+
+ textInput := 1000 * 3e-6
+ textOutput := 500 * 15e-6
+ imageOutput := 100 * 15e-6
+ expectedActual := (textInput + textOutput + imageOutput) * 3.0
+
+ require.InDelta(t, textInput+textOutput+imageOutput, usageRepo.lastLog.TotalCost, 1e-12)
+ require.InDelta(t, imageOutput, usageRepo.lastLog.ImageOutputCost, 1e-12)
+ require.InDelta(t, expectedActual, usageRepo.lastLog.ActualCost, 1e-12)
+ require.InDelta(t, expectedActual, userRepo.lastAmount, 1e-12)
+}
+
func TestGatewayServiceRecordUsage_UsageLogWriteErrorDoesNotSkipBilling(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: false, err: MarkUsageLogCreateNotPersisted(context.Canceled)}
userRepo := &openAIRecordUsageUserRepoStub{}
diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go
index 5d8d8779c2..e4ccced84b 100644
--- a/backend/internal/service/gateway_service.go
+++ b/backend/internal/service/gateway_service.go
@@ -31,6 +31,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"
@@ -9473,7 +9474,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)
+ // token 倍率叠加高峰因子(token 计费含图片 token,图片按次倍率不受影响)。高峰因子按请求时刻现算,
+ // 不并入上面的 getUserGroupRateMultiplier,以免污染 user:group 倍率缓存。
+ multiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, multiplier, timezone.Now())
// 确定计费模型
billingModel := forwardResultBillingModel(result.Model, result.UpstreamModel)
@@ -9676,12 +9679,9 @@ func (s *GatewayService) calculateTokenCost(
})
} else if opts.LongContextThreshold > 0 {
// 长上下文双倍计费(如 Gemini 200K 阈值)
- cost, err = s.billingService.CalculateCostWithLongContext(
- billingModel, tokens, multiplier,
- opts.LongContextThreshold, opts.LongContextMultiplier,
- )
+ cost, err = s.billingService.CalculateCostWithLongContext(billingModel, tokens, multiplier, opts.LongContextThreshold, opts.LongContextMultiplier)
} else {
- cost, err = s.billingService.CalculateCost(billingModel, tokens, multiplier)
+ cost, err = s.billingService.CalculateCostWithServiceTier(billingModel, tokens, multiplier, "")
}
if err != nil {
logger.LegacyPrintf("service.gateway", "Calculate cost failed: %v", err)
diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go
index 9aa2a52f4a..f759e83073 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,9 +19,15 @@ type Group struct {
Description string
Platform string
RateMultiplier float64
- IsExclusive bool
- Status string
- Hydrated bool // indicates the group was loaded from a trusted repository source
+ // 高峰时段倍率:peak_rate_enabled 为 true 且当前时刻处于 [PeakStart, PeakEnd) 时,
+ // token 计费倍率额外乘以 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
SubscriptionType string
DailyLimitUSD *float64
@@ -167,3 +176,80 @@ 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。
+// multiplier=0 是允许的,表示高峰 token 请求按 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 把"基础 token 倍率 base"(已含系统/分组/用户级倍率,但不含高峰)
+// 拆分为最终 token 倍率与图片按次倍率:图片按次倍率基于 base 现算、不受高峰影响;token 倍率在 base 上叠加高峰因子。
+// gateway_service.recordUsageCore 与 openai_gateway_service.RecordUsage 共用此函数,
+// 锁死"高峰因子只乘入 token 倍率、图片按次倍率不受影响"这一叠加顺序——任何调换都会被 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..222b2b70e0
--- /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,验证计费叠加顺序:
+// 图片按次倍率基于基础倍率算出且不受高峰影响,高峰因子只乘入 token 倍率。
+// 若有人调换叠加顺序或把高峰并入 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 token multiplier only", func(t *testing.T) {
+ now := at(15, 30) // 处于 [14:00, 18:00)
+ tokenMultiplier, 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(tokenMultiplier, want) {
+ t.Fatalf("token multiplier should include peak factor: got %v, want %v", tokenMultiplier, want)
+ }
+ })
+
+ t.Run("off-peak leaves both multipliers at base", func(t *testing.T) {
+ now := at(20, 0)
+ tokenMultiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, baseMultiplier, now)
+ if !approxEq(imageMultiplier, baseMultiplier) {
+ t.Fatalf("image multiplier: got %v, want %v", imageMultiplier, baseMultiplier)
+ }
+ if !approxEq(tokenMultiplier, baseMultiplier) {
+ t.Fatalf("token multiplier should equal base off-peak: got %v, want %v", tokenMultiplier, 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)
+ tokenMultiplier, 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(tokenMultiplier, want) {
+ t.Fatalf("token multiplier should include peak factor: got %v, want %v", tokenMultiplier, want)
+ }
+ })
+
+ t.Run("nil api key degrades to base multipliers", func(t *testing.T) {
+ now := at(15, 30)
+ tokenMultiplier, imageMultiplier := computePeakAwareMultipliers(nil, baseMultiplier, now)
+ if !approxEq(tokenMultiplier, baseMultiplier) {
+ t.Fatalf("nil group token multiplier: got %v, want %v", tokenMultiplier, 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_record_usage_test.go b/backend/internal/service/openai_gateway_record_usage_test.go
index 83ca42b733..a7004f5d54 100644
--- a/backend/internal/service/openai_gateway_record_usage_test.go
+++ b/backend/internal/service/openai_gateway_record_usage_test.go
@@ -408,6 +408,72 @@ func TestOpenAIGatewayServiceRecordUsage_UsesUserSpecificGroupRate(t *testing.T)
require.Equal(t, 1, userRepo.deductCalls)
}
+func TestOpenAIGatewayServiceRecordUsage_PeakRateAffectsTokenModeImageOutputTokens(t *testing.T) {
+ groupID := int64(14)
+ groupRate := 1.0
+ usage := OpenAIUsage{
+ InputTokens: 1000,
+ OutputTokens: 600,
+ ImageOutputTokens: 100,
+ }
+
+ usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
+ userRepo := &openAIRecordUsageUserRepoStub{}
+ subRepo := &openAIRecordUsageSubRepoStub{}
+ svc := newOpenAIRecordUsageServiceForTest(usageRepo, userRepo, subRepo, nil)
+ svc.resolver = newOpenAITokenImageChannelPricingResolverForTest(t, groupID, "gpt-5.1")
+
+ err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
+ Result: &OpenAIForwardResult{
+ RequestID: "resp_peak_image_tokens",
+ Usage: usage,
+ Model: "gpt-5.1",
+ Duration: time.Second,
+ ImageCount: 1,
+ },
+ APIKey: &APIKey{
+ ID: 1004,
+ GroupID: i64p(groupID),
+ Group: &Group{
+ ID: groupID,
+ RateMultiplier: groupRate,
+ SubscriptionType: "subscription",
+ PeakRateEnabled: true,
+ PeakStart: "00:00",
+ PeakEnd: "23:59",
+ PeakRateMultiplier: 3.0,
+ },
+ },
+ User: &User{ID: 2004},
+ Account: &Account{ID: 3004},
+ })
+
+ require.NoError(t, err)
+ require.NotNil(t, usageRepo.lastLog)
+ require.Equal(t, 3.0, usageRepo.lastLog.RateMultiplier)
+ require.Equal(t, usage.ImageOutputTokens, usageRepo.lastLog.ImageOutputTokens)
+
+ expected, err := svc.billingService.CalculateCostUnified(CostInput{
+ Ctx: context.Background(),
+ Model: "gpt-5.1",
+ GroupID: i64p(groupID),
+ Tokens: UsageTokens{
+ InputTokens: usage.InputTokens,
+ OutputTokens: usage.OutputTokens,
+ ImageOutputTokens: usage.ImageOutputTokens,
+ },
+ RateMultiplier: 1.0,
+ Resolver: svc.resolver,
+ })
+ require.NoError(t, err)
+ expectedActual := expected.TotalCost * 3.0
+
+ require.InDelta(t, expected.TotalCost, usageRepo.lastLog.TotalCost, 1e-12)
+ require.InDelta(t, expected.ImageOutputCost, usageRepo.lastLog.ImageOutputCost, 1e-12)
+ require.InDelta(t, expectedActual, usageRepo.lastLog.ActualCost, 1e-12)
+ require.InDelta(t, expectedActual, userRepo.lastAmount, 1e-12)
+}
+
func TestOpenAIGatewayServiceRecordUsage_IncludesEndpointMetadata(t *testing.T) {
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
userRepo := &openAIRecordUsageUserRepoStub{}
@@ -1768,6 +1834,26 @@ func newOpenAIImageChannelPricingResolverForTest(t *testing.T, groupID int64, mo
return NewModelPricingResolver(cs, NewBillingService(&config.Config{}, nil))
}
+func newOpenAITokenImageChannelPricingResolverForTest(t *testing.T, groupID int64, model string) *ModelPricingResolver {
+ t.Helper()
+ inputPrice := 3e-6
+ outputPrice := 15e-6
+ imageOutputPrice := 15e-6
+ cache := newEmptyChannelCache()
+ cache.pricingByGroupModel[channelModelKey{groupID: groupID, model: model}] = &ChannelModelPricing{
+ BillingMode: BillingModeToken,
+ InputPrice: &inputPrice,
+ OutputPrice: &outputPrice,
+ ImageOutputPrice: &imageOutputPrice,
+ }
+ cache.channelByGroupID[groupID] = &Channel{ID: groupID, Status: StatusActive}
+ cache.groupPlatform[groupID] = ""
+ cache.loadedAt = time.Now()
+ cs := &ChannelService{}
+ cs.cache.Store(cache)
+ return NewModelPricingResolver(cs, NewBillingService(&config.Config{}, nil))
+}
+
func TestGatewayServiceCalculateRecordUsageCost_ChannelImageBillingUsesImageCount(t *testing.T) {
groupID := int64(126)
billingService := NewBillingService(&config.Config{}, nil)
diff --git a/backend/internal/service/openai_gateway_service.go b/backend/internal/service/openai_gateway_service.go
index 6de5164a9c..da3c3a3636 100644
--- a/backend/internal/service/openai_gateway_service.go
+++ b/backend/internal/service/openai_gateway_service.go
@@ -26,6 +26,7 @@ import (
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai"
"github.com/Wei-Shaw/sub2api/internal/pkg/openai_compat"
+ "github.com/Wei-Shaw/sub2api/internal/pkg/timezone"
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
"github.com/Wei-Shaw/sub2api/internal/util/responseheaders"
"github.com/Wei-Shaw/sub2api/internal/util/urlvalidator"
@@ -6349,7 +6350,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)
+ // token 倍率叠加高峰因子(token 计费含图片 token,图片按次倍率不受影响)。高峰因子按请求时刻现算,
+ // 不并入上面的 Resolve,以免污染 user:group 倍率缓存。
+ multiplier, imageMultiplier := computePeakAwareMultipliers(apiKey, multiplier, timezone.Now())
var cost *CostBreakdown
var err error
diff --git a/backend/internal/service/payment_config_plans.go b/backend/internal/service/payment_config_plans.go
index bb161a1452..8801a55900 100644
--- a/backend/internal/service/payment_config_plans.go
+++ b/backend/internal/service/payment_config_plans.go
@@ -61,13 +61,17 @@ func validatePlanPatch(req UpdatePlanRequest) error {
// PlanGroupInfo holds the group details needed for subscription plan display.
type PlanGroupInfo struct {
- Platform string `json:"platform"`
- Name string `json:"name"`
- RateMultiplier float64 `json:"rate_multiplier"`
- DailyLimitUSD *float64 `json:"daily_limit_usd"`
- WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
- MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
- ModelScopes []string `json:"supported_model_scopes"`
+ Platform string `json:"platform"`
+ Name string `json:"name"`
+ RateMultiplier float64 `json:"rate_multiplier"`
+ PeakRateEnabled bool `json:"peak_rate_enabled"`
+ PeakStart string `json:"peak_start"`
+ PeakEnd string `json:"peak_end"`
+ PeakRateMultiplier float64 `json:"peak_rate_multiplier"`
+ DailyLimitUSD *float64 `json:"daily_limit_usd"`
+ WeeklyLimitUSD *float64 `json:"weekly_limit_usd"`
+ MonthlyLimitUSD *float64 `json:"monthly_limit_usd"`
+ ModelScopes []string `json:"supported_model_scopes"`
}
// GetGroupPlatformMap returns a map of group_id → platform for the given plans.
@@ -100,13 +104,17 @@ func (s *PaymentConfigService) GetGroupInfoMap(ctx context.Context, plans []*dbe
m := make(map[int64]PlanGroupInfo, len(groups))
for _, g := range groups {
m[int64(g.ID)] = PlanGroupInfo{
- Platform: g.Platform,
- Name: g.Name,
- RateMultiplier: g.RateMultiplier,
- DailyLimitUSD: g.DailyLimitUsd,
- WeeklyLimitUSD: g.WeeklyLimitUsd,
- MonthlyLimitUSD: g.MonthlyLimitUsd,
- ModelScopes: g.SupportedModelScopes,
+ Platform: g.Platform,
+ Name: g.Name,
+ RateMultiplier: g.RateMultiplier,
+ PeakRateEnabled: g.PeakRateEnabled,
+ PeakStart: g.PeakStart,
+ PeakEnd: g.PeakEnd,
+ PeakRateMultiplier: g.PeakRateMultiplier,
+ DailyLimitUSD: g.DailyLimitUsd,
+ WeeklyLimitUSD: g.WeeklyLimitUsd,
+ MonthlyLimitUSD: g.MonthlyLimitUsd,
+ ModelScopes: g.SupportedModelScopes,
}
}
return m
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/api/channels.ts b/frontend/src/api/channels.ts
index 8962af2c4d..8eaf7eed95 100644
--- a/frontend/src/api/channels.ts
+++ b/frontend/src/api/channels.ts
@@ -14,6 +14,10 @@ export interface UserAvailableGroup {
subscription_type: string
/** 分组默认倍率。用户专属倍率(若有)通过 /groups/rates 获取后在前端 join。 */
rate_multiplier: number
+ peak_rate_enabled: boolean
+ peak_start: string
+ peak_end: string
+ peak_rate_multiplier: number
/** true = 专属分组(小范围授权);false = 公开分组。 */
is_exclusive: boolean
}
diff --git a/frontend/src/components/admin/user/UserApiKeysModal.vue b/frontend/src/components/admin/user/UserApiKeysModal.vue
index 5e0a0fea69..ef24fe7eb0 100644
--- a/frontend/src/components/admin/user/UserApiKeysModal.vue
+++ b/frontend/src/components/admin/user/UserApiKeysModal.vue
@@ -32,6 +32,10 @@
:platform="key.group.platform"
:subscription-type="key.group.subscription_type"
:rate-multiplier="key.group.rate_multiplier"
+ :peak-rate-enabled="key.group.peak_rate_enabled"
+ :peak-start="key.group.peak_start"
+ :peak-end="key.group.peak_end"
+ :peak-rate-multiplier="key.group.peak_rate_multiplier"
/>
{{ t('admin.users.none') }}
@@ -88,6 +92,10 @@
:platform="group.platform"
:subscription-type="group.subscription_type"
:rate-multiplier="group.rate_multiplier"
+ :peak-rate-enabled="group.peak_rate_enabled"
+ :peak-start="group.peak_start"
+ :peak-end="group.peak_end"
+ :peak-rate-multiplier="group.peak_rate_multiplier"
:description="group.description"
:selected="selectedKeyForGroup?.group_id === group.id"
/>
diff --git a/frontend/src/components/channels/AvailableChannelsTable.vue b/frontend/src/components/channels/AvailableChannelsTable.vue
index 5b9c0eba20..02d1f2d1af 100644
--- a/frontend/src/components/channels/AvailableChannelsTable.vue
+++ b/frontend/src/components/channels/AvailableChannelsTable.vue
@@ -85,16 +85,28 @@
{{ subscription.group.description }}
+