mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3569 from xueshiji/main
feat(group): 订阅分组新增可选的高峰时段倍率,以支持智谱等coding plan的高峰时段
This commit is contained in:
+47
-3
@@ -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(", ")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '月限额',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user