Merge pull request #3569 from xueshiji/main

feat(group): 订阅分组新增可选的高峰时段倍率,以支持智谱等coding plan的高峰时段
This commit is contained in:
Wesley Liddick
2026-07-02 14:55:34 +08:00
committed by GitHub
47 changed files with 2254 additions and 169 deletions
+47 -3
View File
@@ -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(", ")
+44
View File
@@ -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()
+200
View File
@@ -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))
+290
View File
@@ -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) {
+176
View File
@@ -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)
}
+9 -5
View File
@@ -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]},
},
},
}
+250 -1
View File
@@ -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
+40 -20
View File
@@ -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()
+16
View File
@@ -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").
@@ -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,
@@ -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
@@ -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)
}
+4
View File
@@ -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,
+11 -6
View File
@@ -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"`
+50 -33
View File
@@ -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.
@@ -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,
}
+10 -2
View File
@@ -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 {
@@ -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",
+55 -10
View File
@@ -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)
}
@@ -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}
@@ -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 缓存条目,支持负缓存
@@ -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)
+8 -4
View File
@@ -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)
}
@@ -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{
+22 -14
View File
@@ -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,
}
}
@@ -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{}
+6 -6
View File
@@ -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)
+89 -3
View File
@@ -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
}
@@ -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)
}
}
@@ -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)
@@ -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
@@ -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
@@ -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;
+4
View File
@@ -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
}
@@ -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"
/>
<span v-else class="text-gray-400 italic">{{ t('admin.users.none') }}</span>
<svg v-if="updatingKeyIds.has(key.id)" class="h-3 w-3 animate-spin text-primary-500" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
@@ -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"
/>
@@ -85,16 +85,28 @@
<Icon name="shield" size="xs" class="h-3 w-3" />
{{ t('availableChannels.exclusive') }}
</span>
<GroupBadge
<div
v-for="g in exclusiveGroups(section)"
:key="`ex-${g.id}`"
:name="g.name"
:platform="g.platform as GroupPlatform"
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
:rate-multiplier="g.rate_multiplier"
:user-rate-multiplier="userGroupRates[g.id] ?? null"
always-show-rate
/>
class="inline-flex flex-wrap items-center gap-1"
>
<GroupBadge
:name="g.name"
:platform="g.platform as GroupPlatform"
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
:rate-multiplier="g.rate_multiplier"
:user-rate-multiplier="userGroupRates[g.id] ?? null"
always-show-rate
/>
<span
v-if="hasPeakRate(g)"
class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
:title="peakRateTitle(g)"
>
<Icon name="clock" size="xs" class="h-3 w-3" />
{{ peakRateLabel(g) }}
</span>
</div>
</div>
<div
v-if="publicGroups(section).length > 0"
@@ -107,16 +119,28 @@
<Icon name="globe" size="xs" class="h-3 w-3" />
{{ t('availableChannels.public') }}
</span>
<GroupBadge
<div
v-for="g in publicGroups(section)"
:key="`pub-${g.id}`"
:name="g.name"
:platform="g.platform as GroupPlatform"
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
:rate-multiplier="g.rate_multiplier"
:user-rate-multiplier="userGroupRates[g.id] ?? null"
always-show-rate
/>
class="inline-flex flex-wrap items-center gap-1"
>
<GroupBadge
:name="g.name"
:platform="g.platform as GroupPlatform"
:subscription-type="(g.subscription_type || 'standard') as SubscriptionType"
:rate-multiplier="g.rate_multiplier"
:user-rate-multiplier="userGroupRates[g.id] ?? null"
always-show-rate
/>
<span
v-if="hasPeakRate(g)"
class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
:title="peakRateTitle(g)"
>
<Icon name="clock" size="xs" class="h-3 w-3" />
{{ peakRateLabel(g) }}
</span>
</div>
</div>
<span v-if="section.groups.length === 0" class="text-xs text-gray-400">-</span>
</div>
@@ -186,4 +210,16 @@ function exclusiveGroups(section: UserChannelPlatformSection): UserAvailableGrou
function publicGroups(section: UserChannelPlatformSection): UserAvailableGroup[] {
return section.groups.filter((g) => !g.is_exclusive)
}
function hasPeakRate(group: UserAvailableGroup): boolean {
return Boolean(group.peak_rate_enabled && group.peak_start && group.peak_end)
}
function peakRateLabel(group: UserAvailableGroup): string {
return `${group.peak_start}-${group.peak_end} ${group.peak_rate_multiplier}x`
}
function peakRateTitle(group: UserAvailableGroup): string {
return `高峰倍率:${group.peak_start}-${group.peak_end} ${group.peak_rate_multiplier}x;token 计费的图片 token 同样适用,图片按次计费不受高峰影响`
}
</script>
@@ -20,6 +20,9 @@
{{ labelText }}
</template>
</span>
<span v-if="hasPeakRate" :class="peakRateClass" :title="peakRateTitle">
{{ peakRateText }}
</span>
</span>
</template>
@@ -35,6 +38,10 @@ interface Props {
subscriptionType?: SubscriptionType
rateMultiplier?: number
userRateMultiplier?: number | null // 用户专属倍率
peakRateEnabled?: boolean
peakStart?: string
peakEnd?: string
peakRateMultiplier?: number
showRate?: boolean
daysRemaining?: number | null // 剩余天数(订阅类型时使用)
/**
@@ -50,6 +57,7 @@ const props = withDefaults(defineProps<Props>(), {
showRate: true,
daysRemaining: null,
userRateMultiplier: null,
peakRateEnabled: false,
alwaysShowRate: false
})
@@ -67,6 +75,18 @@ const hasCustomRate = computed(() => {
)
})
const hasPeakRate = computed(() => {
return Boolean(props.showRate && props.peakRateEnabled && props.peakStart && props.peakEnd)
})
const peakRateText = computed(() => {
return `${props.peakStart}-${props.peakEnd} ×${props.peakRateMultiplier ?? 1}`
})
const peakRateTitle = computed(() => {
return `高峰倍率:${peakRateText.value}`
})
// 是否显示右侧标签
const showLabel = computed(() => {
if (!props.showRate) return false
@@ -133,6 +153,10 @@ const labelClass = computed(() => {
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
})
const peakRateClass = computed(() => {
return 'px-1.5 py-0.5 rounded text-[10px] font-semibold bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
})
// Badge color based on platform and subscription type
const badgeClass = computed(() => {
if (props.platform === 'anthropic') {
@@ -24,16 +24,25 @@
<!-- Right: rate pill + checkmark (vertically centered to first row) -->
<div class="flex shrink-0 items-center gap-2 pt-0.5">
<!-- Rate pill (platform color) -->
<span v-if="rateMultiplier !== undefined" :class="['inline-flex items-center whitespace-nowrap rounded-full px-3 py-1 text-xs font-semibold', ratePillClass]">
<template v-if="hasCustomRate">
<span class="mr-1 line-through opacity-50">{{ rateMultiplier }}x</span>
<span class="font-bold">{{ userRateMultiplier }}x</span>
</template>
<template v-else>
{{ rateMultiplier }}x {{ t('admin.groups.rateLabel') }}
</template>
</span>
<div class="flex shrink-0 flex-col items-end gap-1">
<!-- Rate pill (platform color) -->
<span v-if="rateMultiplier !== undefined" :class="['inline-flex items-center whitespace-nowrap rounded-full px-3 py-1 text-xs font-semibold', ratePillClass]">
<template v-if="hasCustomRate">
<span class="mr-1 line-through opacity-50">{{ rateMultiplier }}x</span>
<span class="font-bold">{{ userRateMultiplier }}x</span>
</template>
<template v-else>
{{ rateMultiplier }}x {{ t('admin.groups.rateLabel') }}
</template>
</span>
<span
v-if="hasPeakRate"
class="inline-flex items-center whitespace-nowrap rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-700 dark:bg-amber-900/20 dark:text-amber-300"
:title="peakRateTitle"
>
{{ peakRateText }}
</span>
</div>
<!-- Checkmark -->
<svg
v-if="showCheckmark && selected"
@@ -63,6 +72,10 @@ interface Props {
subscriptionType?: SubscriptionType
rateMultiplier?: number
userRateMultiplier?: number | null
peakRateEnabled?: boolean
peakStart?: string
peakEnd?: string
peakRateMultiplier?: number
description?: string | null
selected?: boolean
showCheckmark?: boolean
@@ -72,7 +85,8 @@ const props = withDefaults(defineProps<Props>(), {
subscriptionType: 'standard',
selected: false,
showCheckmark: true,
userRateMultiplier: null
userRateMultiplier: null,
peakRateEnabled: false
})
// Whether user has a custom rate different from default
@@ -85,6 +99,18 @@ const hasCustomRate = computed(() => {
)
})
const hasPeakRate = computed(() => {
return Boolean(props.peakRateEnabled && props.peakStart && props.peakEnd)
})
const peakRateText = computed(() => {
return `${props.peakStart}-${props.peakEnd} ×${props.peakRateMultiplier ?? 1}`
})
const peakRateTitle = computed(() => {
return `高峰倍率:${peakRateText.value}`
})
// Rate pill color matches platform badge color
const ratePillClass = computed(() => {
switch (props.platform) {
@@ -43,6 +43,10 @@
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.rate') }}</span>
<span class="font-medium text-gray-700 dark:text-gray-300">{{ rateDisplay }}</span>
</div>
<div v-if="hasPeakRate" class="col-span-2 flex items-center justify-between gap-2">
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.peakRate') }}</span>
<span class="text-right font-medium text-amber-700 dark:text-amber-300">{{ peakRateDisplay }}</span>
</div>
<div v-if="plan.daily_limit_usd != null" class="flex items-center justify-between">
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.dailyLimit') }}</span>
<span class="font-medium text-gray-700 dark:text-gray-300">${{ plan.daily_limit_usd }}</span>
@@ -140,6 +144,14 @@ const rateDisplay = computed(() => {
return `×${Number(rate.toPrecision(10))}`
})
const hasPeakRate = computed(() => {
return Boolean(props.plan.peak_rate_enabled && props.plan.peak_start && props.plan.peak_end)
})
const peakRateDisplay = computed(() => {
return `${props.plan.peak_start}-${props.plan.peak_end} ×${props.plan.peak_rate_multiplier ?? 1}`
})
const MODEL_SCOPE_LABELS: Record<string, string> = {
claude: 'Claude',
gemini_text: 'Gemini',
+8
View File
@@ -2279,6 +2279,13 @@ export default {
finalPricePreview: 'Final per-image price preview',
notConfigured: 'Not configured'
},
peakRate: {
enable: 'Enable peak rate multiplier',
peakStart: 'Peak start',
peakEnd: 'Peak end',
peakMultiplier: 'Peak multiplier',
multiplierHint: 'Applies to token billing multiplier; image tokens in token billing are also affected. 0 means peak token requests are billed at 0x.'
},
modelsList: {
title: 'Custom /v1/models Model List',
hint: 'Only changes the /v1/models response. Whitelist model calls and account routing are unchanged.',
@@ -7265,6 +7272,7 @@ export default {
planFeatures: 'Features',
planCard: {
rate: 'Rate',
peakRate: 'Peak Rate',
dailyLimit: 'Daily',
weeklyLimit: 'Weekly',
monthlyLimit: 'Monthly',
+8
View File
@@ -2362,6 +2362,13 @@ export default {
finalPricePreview: '最终单张价格预览',
notConfigured: '未配置'
},
peakRate: {
enable: '启用高峰倍率',
peakStart: '高峰开始',
peakEnd: '高峰结束',
peakMultiplier: '高峰倍率',
multiplierHint: '作用于 token 计费倍率;token 计费的图片 token 同样适用,0 表示高峰 token 请求按 0 倍计费'
},
modelsList: {
title: '自定义 /v1/models 模型列表',
hint: '仅影响 /v1/models 展示结果,不影响白名单模型调用和账号调度。',
@@ -7445,6 +7452,7 @@ export default {
planFeatures: '功能特性',
planCard: {
rate: '倍率',
peakRate: '高峰倍率',
dailyLimit: '日限额',
weeklyLimit: '周限额',
monthlyLimit: '月限额',
+13
View File
@@ -518,6 +518,11 @@ export interface Group {
image_price_1k: number | null
image_price_2k: number | null
image_price_4k: number | null
// 高峰时段倍率配置
peak_rate_enabled: boolean
peak_start: string
peak_end: string
peak_rate_multiplier: number
// Claude Code 客户端限制
claude_code_only: boolean
fallback_group_id: number | null
@@ -636,6 +641,10 @@ export interface CreateGroupRequest {
image_price_1k?: number | null
image_price_2k?: number | null
image_price_4k?: number | null
peak_rate_enabled?: boolean
peak_start?: string
peak_end?: string
peak_rate_multiplier?: number
claude_code_only?: boolean
fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null
@@ -671,6 +680,10 @@ export interface UpdateGroupRequest {
image_price_1k?: number | null
image_price_2k?: number | null
image_price_4k?: number | null
peak_rate_enabled?: boolean
peak_start?: string
peak_end?: string
peak_rate_multiplier?: number
claude_code_only?: boolean
fallback_group_id?: number | null
fallback_group_id_on_invalid_request?: number | null
+4
View File
@@ -108,6 +108,10 @@ export interface SubscriptionPlan {
group_platform?: string
group_name?: string
rate_multiplier?: number
peak_rate_enabled?: boolean
peak_start?: string
peak_end?: string
peak_rate_multiplier?: number
daily_limit_usd?: number | null
weekly_limit_usd?: number | null
monthly_limit_usd?: number | null
+154 -4
View File
@@ -859,6 +859,53 @@
</div>
</div>
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
<div v-if="createForm.subscription_type === 'subscription'" class="border-t pt-4">
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="createForm.peak_rate_enabled"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>{{ t("admin.groups.peakRate.enable") }}</span>
</label>
</div>
<div
v-if="createForm.peak_rate_enabled"
class="mb-4 grid grid-cols-3 gap-3"
>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
<input
v-model="createForm.peak_start"
type="time"
class="input"
/>
</div>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakEnd") }}</label>
<input
v-model="createForm.peak_end"
type="time"
class="input"
/>
</div>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakMultiplier") }}</label>
<input
v-model.number="createForm.peak_rate_multiplier"
type="number"
step="0.001"
min="0"
class="input"
placeholder="1"
:title="t('admin.groups.peakRate.multiplierHint')"
/>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
<div v-if="createForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
@@ -2151,6 +2198,53 @@
</div>
</div>
<!-- 高峰时段倍率配置(仅订阅类型分组) -->
<div v-if="editForm.subscription_type === 'subscription'" class="border-t pt-4">
<div class="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
<input
v-model="editForm.peak_rate_enabled"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<span>{{ t("admin.groups.peakRate.enable") }}</span>
</label>
</div>
<div
v-if="editForm.peak_rate_enabled"
class="mb-4 grid grid-cols-3 gap-3"
>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakStart") }}</label>
<input
v-model="editForm.peak_start"
type="time"
class="input"
/>
</div>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakEnd") }}</label>
<input
v-model="editForm.peak_end"
type="time"
class="input"
/>
</div>
<div>
<label class="input-label">{{ t("admin.groups.peakRate.peakMultiplier") }}</label>
<input
v-model.number="editForm.peak_rate_multiplier"
type="number"
step="0.001"
min="0"
class="input"
placeholder="1"
:title="t('admin.groups.peakRate.multiplierHint')"
/>
</div>
</div>
</div>
<!-- 支持的模型系列(仅 antigravity 平台) -->
<div v-if="editForm.platform === 'antigravity'" class="border-t pt-4">
<div class="mb-1.5 flex items-center gap-1">
@@ -3354,6 +3448,11 @@ const createForm = reactive({
image_price_1k: null as number | null,
image_price_2k: null as number | null,
image_price_4k: null as number | null,
// 高峰时段倍率配置
peak_rate_enabled: false,
peak_start: "",
peak_end: "",
peak_rate_multiplier: 1.0,
// Claude Code 客户端限制(仅 anthropic 平台使用)
claude_code_only: false,
fallback_group_id: null as number | null,
@@ -3685,6 +3784,11 @@ const editForm = reactive({
image_price_1k: null as number | null,
image_price_2k: null as number | null,
image_price_4k: null as number | null,
// 高峰时段倍率配置
peak_rate_enabled: false,
peak_start: "",
peak_end: "",
peak_rate_multiplier: 1.0,
// Claude Code 客户端限制(仅 anthropic 平台使用)
claude_code_only: false,
fallback_group_id: null as number | null,
@@ -3718,6 +3822,10 @@ type ImagePricingFormState = {
image_price_1k: number | string | null;
image_price_2k: number | string | null;
image_price_4k: number | string | null;
peak_rate_enabled: boolean;
peak_start: string;
peak_end: string;
peak_rate_multiplier: number;
};
const imagePricingTiers = [
@@ -3936,6 +4044,10 @@ const closeCreateModal = () => {
createForm.image_price_1k = null;
createForm.image_price_2k = null;
createForm.image_price_4k = null;
createForm.peak_rate_enabled = false;
createForm.peak_start = "";
createForm.peak_end = "";
createForm.peak_rate_multiplier = 1.0;
createForm.claude_code_only = false;
createForm.fallback_group_id = null;
createForm.fallback_group_id_on_invalid_request = null;
@@ -3969,7 +4081,7 @@ const normalizeOptionalLimit = (
return Number.isFinite(value) && value > 0 ? value : null;
};
const normalizeImageRateMultiplier = (
const normalizeRateMultiplier = (
value: number | string | null | undefined,
): number => {
if (value === null || value === undefined || value === "") {
@@ -4022,9 +4134,15 @@ const handleCreateGroup = async () => {
requestData.daily_limit_usd = emptyToNull(requestData.daily_limit_usd);
requestData.weekly_limit_usd = emptyToNull(requestData.weekly_limit_usd);
requestData.monthly_limit_usd = emptyToNull(requestData.monthly_limit_usd);
requestData.image_rate_multiplier = normalizeImageRateMultiplier(
requestData.image_rate_multiplier = normalizeRateMultiplier(
requestData.image_rate_multiplier,
);
requestData.peak_rate_enabled = createForm.peak_rate_enabled;
requestData.peak_start = createForm.peak_start;
requestData.peak_end = createForm.peak_end;
requestData.peak_rate_multiplier = normalizeRateMultiplier(
createForm.peak_rate_multiplier,
);
await adminAPI.groups.create(requestData);
appStore.showSuccess(t("admin.groups.groupCreated"));
closeCreateModal();
@@ -4062,6 +4180,10 @@ const handleEdit = async (group: AdminGroup) => {
editForm.image_price_1k = group.image_price_1k;
editForm.image_price_2k = group.image_price_2k;
editForm.image_price_4k = group.image_price_4k;
editForm.peak_rate_enabled = group.peak_rate_enabled ?? false;
editForm.peak_start = group.peak_start ?? "";
editForm.peak_end = group.peak_end ?? "";
editForm.peak_rate_multiplier = group.peak_rate_multiplier ?? 1.0;
editForm.claude_code_only = group.claude_code_only || false;
editForm.fallback_group_id = group.fallback_group_id;
editForm.fallback_group_id_on_invalid_request =
@@ -4106,6 +4228,10 @@ const closeEditModal = () => {
editingGroup.value = null;
editModelRoutingRules.value = [];
editForm.copy_accounts_from_group_ids = [];
editForm.peak_rate_enabled = false;
editForm.peak_start = "";
editForm.peak_end = "";
editForm.peak_rate_multiplier = 1.0;
resetMessagesDispatchFormState(editForm);
resetModelsListState(editModelsListState);
};
@@ -4161,9 +4287,15 @@ const handleUpdateGroup = async () => {
payload.daily_limit_usd = emptyToNull(payload.daily_limit_usd);
payload.weekly_limit_usd = emptyToNull(payload.weekly_limit_usd);
payload.monthly_limit_usd = emptyToNull(payload.monthly_limit_usd);
payload.image_rate_multiplier = normalizeImageRateMultiplier(
payload.image_rate_multiplier = normalizeRateMultiplier(
payload.image_rate_multiplier,
);
payload.peak_rate_enabled = editForm.peak_rate_enabled;
payload.peak_start = editForm.peak_start;
payload.peak_end = editForm.peak_end;
payload.peak_rate_multiplier = normalizeRateMultiplier(
editForm.peak_rate_multiplier,
);
await adminAPI.groups.update(editingGroup.value.id, payload);
appStore.showSuccess(t("admin.groups.groupUpdated"));
closeEditModal();
@@ -4234,13 +4366,31 @@ const confirmDelete = async () => {
}
};
// 监听 subscription_type 变化,订阅模式时 is_exclusive 默认为 true
// 监听 subscription_type 变化,订阅模式时 is_exclusive 默认为 true;标准模式清空高峰配置
watch(
() => createForm.subscription_type,
(newVal) => {
if (newVal === "subscription") {
createForm.is_exclusive = true;
createForm.fallback_group_id_on_invalid_request = null;
} else {
createForm.peak_rate_enabled = false;
createForm.peak_start = "";
createForm.peak_end = "";
createForm.peak_rate_multiplier = 1.0;
}
},
);
// 编辑表单:切回标准模式时清空高峰配置,避免残留随更新请求提交被后端拒绝
watch(
() => editForm.subscription_type,
(newVal) => {
if (newVal !== "subscription") {
editForm.peak_rate_enabled = false;
editForm.peak_start = "";
editForm.peak_end = "";
editForm.peak_rate_multiplier = 1.0;
}
},
);
+24
View File
@@ -144,6 +144,10 @@
:subscription-type="row.group.subscription_type"
:rate-multiplier="row.group.rate_multiplier"
:user-rate-multiplier="userGroupRates[row.group.id]"
:peak-rate-enabled="row.group.peak_rate_enabled"
:peak-start="row.group.peak_start"
:peak-end="row.group.peak_end"
:peak-rate-multiplier="row.group.peak_rate_multiplier"
/>
<span v-else class="text-sm text-gray-400 dark:text-dark-500">{{
t('keys.noGroup')
@@ -454,6 +458,10 @@
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
:user-rate-multiplier="(option as unknown as GroupOption).userRate"
:peak-rate-enabled="(option as unknown as GroupOption).peakRateEnabled"
:peak-start="(option as unknown as GroupOption).peakStart"
:peak-end="(option as unknown as GroupOption).peakEnd"
:peak-rate-multiplier="(option as unknown as GroupOption).peakRateMultiplier"
/>
<span v-else class="text-gray-400">{{ t('keys.selectGroup') }}</span>
</template>
@@ -464,6 +472,10 @@
:subscription-type="(option as unknown as GroupOption).subscriptionType"
:rate-multiplier="(option as unknown as GroupOption).rate"
:user-rate-multiplier="(option as unknown as GroupOption).userRate"
:peak-rate-enabled="(option as unknown as GroupOption).peakRateEnabled"
:peak-start="(option as unknown as GroupOption).peakStart"
:peak-end="(option as unknown as GroupOption).peakEnd"
:peak-rate-multiplier="(option as unknown as GroupOption).peakRateMultiplier"
:description="(option as unknown as GroupOption).description"
:selected="selected"
/>
@@ -1059,6 +1071,10 @@
:subscription-type="option.subscriptionType"
:rate-multiplier="option.rate"
:user-rate-multiplier="option.userRate"
:peak-rate-enabled="option.peakRateEnabled"
:peak-start="option.peakStart"
:peak-end="option.peakEnd"
:peak-rate-multiplier="option.peakRateMultiplier"
:description="option.description"
:selected="
selectedKeyForGroup?.group_id === option.value ||
@@ -1123,6 +1139,10 @@ interface GroupOption {
description: string | null
rate: number
userRate: number | null
peakRateEnabled: boolean
peakStart: string
peakEnd: string
peakRateMultiplier: number
subscriptionType: SubscriptionType
platform: GroupPlatform
}
@@ -1351,6 +1371,10 @@ const groupOptions = computed(() =>
description: group.description,
rate: group.rate_multiplier,
userRate: userGroupRates.value[group.id] ?? null,
peakRateEnabled: group.peak_rate_enabled,
peakStart: group.peak_start,
peakEnd: group.peak_end,
peakRateMultiplier: group.peak_rate_multiplier,
subscriptionType: group.subscription_type,
platform: group.platform
}))
+25
View File
@@ -121,6 +121,12 @@
<span :class="['text-lg font-bold', planTextClass]">×{{ selectedPlan.rate_multiplier ?? 1 }}</span>
</div>
</div>
<div v-if="planHasPeakRate(selectedPlan)">
<span class="text-xs text-gray-400 dark:text-gray-500">{{ t('payment.planCard.peakRate') }}</span>
<div class="text-sm font-semibold text-amber-700 dark:text-amber-300">
{{ planPeakRateLabel(selectedPlan) }}
</div>
</div>
<div v-if="selectedPlan.daily_limit_usd != null">
<span class="text-xs text-gray-400 dark:text-gray-500">{{ t('payment.planCard.dailyLimit') }}</span>
<div class="text-lg font-semibold text-gray-800 dark:text-gray-200">${{ selectedPlan.daily_limit_usd }}</div>
@@ -194,6 +200,7 @@
</div>
<div class="flex flex-wrap gap-x-3 text-[11px] text-gray-400 dark:text-gray-500">
<span>{{ t('payment.planCard.rate') }}: ×{{ sub.group?.rate_multiplier ?? 1 }}</span>
<span v-if="subscriptionHasPeakRate(sub)">{{ t('payment.planCard.peakRate') }}: {{ subscriptionPeakRateLabel(sub) }}</span>
<span v-if="sub.group?.daily_limit_usd == null && sub.group?.weekly_limit_usd == null && sub.group?.monthly_limit_usd == null">{{ t('payment.planCard.quota') }}: {{ t('payment.planCard.unlimited') }}</span>
<span v-if="sub.expires_at">{{ t('userSubscriptions.daysRemaining', { days: getDaysRemaining(sub.expires_at) }) }}</span>
<span v-else>{{ t('userSubscriptions.noExpiration') }}</span>
@@ -297,6 +304,16 @@ function getDaysRemaining(expiresAt: string): number {
return Math.max(0, Math.ceil(diff / (1000 * 60 * 60 * 24)))
}
function subscriptionHasPeakRate(sub: { group?: { peak_rate_enabled?: boolean; peak_start?: string; peak_end?: string } | null }): boolean {
const group = sub.group
return Boolean(group?.peak_rate_enabled && group.peak_start && group.peak_end)
}
function subscriptionPeakRateLabel(sub: { group?: { peak_start?: string; peak_end?: string; peak_rate_multiplier?: number } | null }): string {
const group = sub.group
return `${group?.peak_start}-${group?.peak_end} ×${group?.peak_rate_multiplier ?? 1}`
}
const loading = ref(true)
const submitting = ref(false)
const errorMessage = ref('')
@@ -695,6 +712,14 @@ const planValiditySuffix = computed(() => {
return `${selectedPlan.value.validity_days}${t('payment.days')}`
})
function planHasPeakRate(plan: SubscriptionPlan): boolean {
return Boolean(plan.peak_rate_enabled && plan.peak_start && plan.peak_end)
}
function planPeakRateLabel(plan: SubscriptionPlan): string {
return `${plan.peak_start}-${plan.peak_end} ×${plan.peak_rate_multiplier ?? 1}`
}
function selectPlan(plan: SubscriptionPlan) {
selectedPlan.value = plan
errorMessage.value = ''
@@ -49,6 +49,12 @@
<p v-if="subscription.group?.description" class="mt-0.5 text-xs text-gray-500 dark:text-dark-400">
{{ subscription.group.description }}
</p>
<div class="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-gray-400 dark:text-gray-500">
<span>{{ t('payment.planCard.rate') }}: ×{{ subscription.group?.rate_multiplier ?? 1 }}</span>
<span v-if="subscriptionHasPeakRate(subscription)" class="text-amber-700 dark:text-amber-300">
{{ t('payment.planCard.peakRate') }}: {{ subscriptionPeakRateLabel(subscription) }}
</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
@@ -271,6 +277,16 @@ const appStore = useAppStore()
const subscriptions = ref<UserSubscription[]>([])
const loading = ref(true)
function subscriptionHasPeakRate(subscription: UserSubscription): boolean {
const group = subscription.group
return Boolean(group?.peak_rate_enabled && group.peak_start && group.peak_end)
}
function subscriptionPeakRateLabel(subscription: UserSubscription): string {
const group = subscription.group
return `${group?.peak_start}-${group?.peak_end} ×${group?.peak_rate_multiplier ?? 1}`
}
async function loadSubscriptions() {
try {
loading.value = true