refactor(grok): 将视频模型价纳入 Ent 持久化

This commit is contained in:
IanShaw027
2026-08-07 13:55:58 +08:00
parent e12e0dc1a6
commit dfca6246eb
15 changed files with 292 additions and 177 deletions
+14 -1
View File
@@ -85,6 +85,8 @@ type Group struct {
VideoPrice720p *float64 `json:"video_price_720p,omitempty"`
// VideoPrice1080p holds the value of the "video_price_1080p" field.
VideoPrice1080p *float64 `json:"video_price_1080p,omitempty"`
// 按模型族和分辨率覆盖视频每秒价格
VideoModelPrices map[string]map[string]float64 `json:"video_model_prices,omitempty"`
// Codex alpha/search 网页搜索单次价格(USD/次);nil 表示使用默认价 0.01(官方 $10/1000 次)
WebSearchPricePerCall *float64 `json:"web_search_price_per_call,omitempty"`
// 是否仅允许 Claude Code 客户端
@@ -235,7 +237,7 @@ func (*Group) scanValues(columns []string) ([]any, error) {
values := make([]any, len(columns))
for i := range columns {
switch columns[i] {
case group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldReasoningEffortMappings:
case group.FieldVideoModelPrices, group.FieldModelRouting, group.FieldSupportedModelScopes, group.FieldMessagesDispatchModelConfig, group.FieldModelsListConfig, group.FieldReasoningEffortMappings:
values[i] = new([]byte)
case group.FieldPeakRateEnabled, group.FieldIsExclusive, group.FieldAllowImageGeneration, group.FieldAllowBatchImageGeneration, group.FieldImageRateIndependent, group.FieldVideoRateIndependent, group.FieldClaudeCodeOnly, group.FieldModelRoutingEnabled, group.FieldMcpXMLInject, group.FieldAllowMessagesDispatch, group.FieldAllowLive, group.FieldRequireOauthOnly, group.FieldRequirePrivacySet, group.FieldProfitControlEnabled:
values[i] = new(sql.NullBool)
@@ -478,6 +480,14 @@ func (_m *Group) assignValues(columns []string, values []any) error {
_m.VideoPrice1080p = new(float64)
*_m.VideoPrice1080p = value.Float64
}
case group.FieldVideoModelPrices:
if value, ok := values[i].(*[]byte); !ok {
return fmt.Errorf("unexpected type %T for field video_model_prices", values[i])
} else if value != nil && len(*value) > 0 {
if err := json.Unmarshal(*value, &_m.VideoModelPrices); err != nil {
return fmt.Errorf("unmarshal field video_model_prices: %w", err)
}
}
case group.FieldWebSearchPricePerCall:
if value, ok := values[i].(*sql.NullFloat64); !ok {
return fmt.Errorf("unexpected type %T for field web_search_price_per_call", values[i])
@@ -822,6 +832,9 @@ func (_m *Group) String() string {
builder.WriteString(fmt.Sprintf("%v", *v))
}
builder.WriteString(", ")
builder.WriteString("video_model_prices=")
builder.WriteString(fmt.Sprintf("%v", _m.VideoModelPrices))
builder.WriteString(", ")
if v := _m.WebSearchPricePerCall; v != nil {
builder.WriteString("web_search_price_per_call=")
builder.WriteString(fmt.Sprintf("%v", *v))
+3
View File
@@ -82,6 +82,8 @@ const (
FieldVideoPrice720p = "video_price_720p"
// FieldVideoPrice1080p holds the string denoting the video_price_1080p field in the database.
FieldVideoPrice1080p = "video_price_1080p"
// FieldVideoModelPrices holds the string denoting the video_model_prices field in the database.
FieldVideoModelPrices = "video_model_prices"
// FieldWebSearchPricePerCall holds the string denoting the web_search_price_per_call field in the database.
FieldWebSearchPricePerCall = "web_search_price_per_call"
// FieldClaudeCodeOnly holds the string denoting the claude_code_only field in the database.
@@ -234,6 +236,7 @@ var Columns = []string{
FieldVideoPrice480p,
FieldVideoPrice720p,
FieldVideoPrice1080p,
FieldVideoModelPrices,
FieldWebSearchPricePerCall,
FieldClaudeCodeOnly,
FieldFallbackGroupID,
+10
View File
@@ -1765,6 +1765,16 @@ func VideoPrice1080pNotNil() predicate.Group {
return predicate.Group(sql.FieldNotNull(FieldVideoPrice1080p))
}
// VideoModelPricesIsNil applies the IsNil predicate on the "video_model_prices" field.
func VideoModelPricesIsNil() predicate.Group {
return predicate.Group(sql.FieldIsNull(FieldVideoModelPrices))
}
// VideoModelPricesNotNil applies the NotNil predicate on the "video_model_prices" field.
func VideoModelPricesNotNil() predicate.Group {
return predicate.Group(sql.FieldNotNull(FieldVideoModelPrices))
}
// WebSearchPricePerCallEQ applies the EQ predicate on the "web_search_price_per_call" field.
func WebSearchPricePerCallEQ(v float64) predicate.Group {
return predicate.Group(sql.FieldEQ(FieldWebSearchPricePerCall, v))
+70
View File
@@ -483,6 +483,12 @@ func (_c *GroupCreate) SetNillableVideoPrice1080p(v *float64) *GroupCreate {
return _c
}
// SetVideoModelPrices sets the "video_model_prices" field.
func (_c *GroupCreate) SetVideoModelPrices(v map[string]map[string]float64) *GroupCreate {
_c.mutation.SetVideoModelPrices(v)
return _c
}
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
func (_c *GroupCreate) SetWebSearchPricePerCall(v float64) *GroupCreate {
_c.mutation.SetWebSearchPricePerCall(v)
@@ -1378,6 +1384,10 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) {
_spec.SetField(group.FieldVideoPrice1080p, field.TypeFloat64, value)
_node.VideoPrice1080p = &value
}
if value, ok := _c.mutation.VideoModelPrices(); ok {
_spec.SetField(group.FieldVideoModelPrices, field.TypeJSON, value)
_node.VideoModelPrices = value
}
if value, ok := _c.mutation.WebSearchPricePerCall(); ok {
_spec.SetField(group.FieldWebSearchPricePerCall, field.TypeFloat64, value)
_node.WebSearchPricePerCall = &value
@@ -2156,6 +2166,24 @@ func (u *GroupUpsert) ClearVideoPrice1080p() *GroupUpsert {
return u
}
// SetVideoModelPrices sets the "video_model_prices" field.
func (u *GroupUpsert) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpsert {
u.Set(group.FieldVideoModelPrices, v)
return u
}
// UpdateVideoModelPrices sets the "video_model_prices" field to the value that was provided on create.
func (u *GroupUpsert) UpdateVideoModelPrices() *GroupUpsert {
u.SetExcluded(group.FieldVideoModelPrices)
return u
}
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
func (u *GroupUpsert) ClearVideoModelPrices() *GroupUpsert {
u.SetNull(group.FieldVideoModelPrices)
return u
}
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
func (u *GroupUpsert) SetWebSearchPricePerCall(v float64) *GroupUpsert {
u.Set(group.FieldWebSearchPricePerCall, v)
@@ -3157,6 +3185,27 @@ func (u *GroupUpsertOne) ClearVideoPrice1080p() *GroupUpsertOne {
})
}
// SetVideoModelPrices sets the "video_model_prices" field.
func (u *GroupUpsertOne) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.SetVideoModelPrices(v)
})
}
// UpdateVideoModelPrices sets the "video_model_prices" field to the value that was provided on create.
func (u *GroupUpsertOne) UpdateVideoModelPrices() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.UpdateVideoModelPrices()
})
}
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
func (u *GroupUpsertOne) ClearVideoModelPrices() *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
s.ClearVideoModelPrices()
})
}
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
func (u *GroupUpsertOne) SetWebSearchPricePerCall(v float64) *GroupUpsertOne {
return u.Update(func(s *GroupUpsert) {
@@ -4379,6 +4428,27 @@ func (u *GroupUpsertBulk) ClearVideoPrice1080p() *GroupUpsertBulk {
})
}
// SetVideoModelPrices sets the "video_model_prices" field.
func (u *GroupUpsertBulk) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.SetVideoModelPrices(v)
})
}
// UpdateVideoModelPrices sets the "video_model_prices" field to the value that was provided on create.
func (u *GroupUpsertBulk) UpdateVideoModelPrices() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.UpdateVideoModelPrices()
})
}
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
func (u *GroupUpsertBulk) ClearVideoModelPrices() *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
s.ClearVideoModelPrices()
})
}
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
func (u *GroupUpsertBulk) SetWebSearchPricePerCall(v float64) *GroupUpsertBulk {
return u.Update(func(s *GroupUpsert) {
+36
View File
@@ -640,6 +640,18 @@ func (_u *GroupUpdate) ClearVideoPrice1080p() *GroupUpdate {
return _u
}
// SetVideoModelPrices sets the "video_model_prices" field.
func (_u *GroupUpdate) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpdate {
_u.mutation.SetVideoModelPrices(v)
return _u
}
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
func (_u *GroupUpdate) ClearVideoModelPrices() *GroupUpdate {
_u.mutation.ClearVideoModelPrices()
return _u
}
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
func (_u *GroupUpdate) SetWebSearchPricePerCall(v float64) *GroupUpdate {
_u.mutation.ResetWebSearchPricePerCall()
@@ -1506,6 +1518,12 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) {
if _u.mutation.VideoPrice1080pCleared() {
_spec.ClearField(group.FieldVideoPrice1080p, field.TypeFloat64)
}
if value, ok := _u.mutation.VideoModelPrices(); ok {
_spec.SetField(group.FieldVideoModelPrices, field.TypeJSON, value)
}
if _u.mutation.VideoModelPricesCleared() {
_spec.ClearField(group.FieldVideoModelPrices, field.TypeJSON)
}
if value, ok := _u.mutation.WebSearchPricePerCall(); ok {
_spec.SetField(group.FieldWebSearchPricePerCall, field.TypeFloat64, value)
}
@@ -2533,6 +2551,18 @@ func (_u *GroupUpdateOne) ClearVideoPrice1080p() *GroupUpdateOne {
return _u
}
// SetVideoModelPrices sets the "video_model_prices" field.
func (_u *GroupUpdateOne) SetVideoModelPrices(v map[string]map[string]float64) *GroupUpdateOne {
_u.mutation.SetVideoModelPrices(v)
return _u
}
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
func (_u *GroupUpdateOne) ClearVideoModelPrices() *GroupUpdateOne {
_u.mutation.ClearVideoModelPrices()
return _u
}
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
func (_u *GroupUpdateOne) SetWebSearchPricePerCall(v float64) *GroupUpdateOne {
_u.mutation.ResetWebSearchPricePerCall()
@@ -3429,6 +3459,12 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error)
if _u.mutation.VideoPrice1080pCleared() {
_spec.ClearField(group.FieldVideoPrice1080p, field.TypeFloat64)
}
if value, ok := _u.mutation.VideoModelPrices(); ok {
_spec.SetField(group.FieldVideoModelPrices, field.TypeJSON, value)
}
if _u.mutation.VideoModelPricesCleared() {
_spec.ClearField(group.FieldVideoModelPrices, field.TypeJSON)
}
if value, ok := _u.mutation.WebSearchPricePerCall(); ok {
_spec.SetField(group.FieldWebSearchPricePerCall, field.TypeFloat64, value)
}
+2 -1
View File
@@ -928,6 +928,7 @@ var (
{Name: "video_price_480p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "video_price_720p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "video_price_1080p", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "video_model_prices", Type: field.TypeJSON, Nullable: true, SchemaType: map[string]string{"postgres": "jsonb"}},
{Name: "web_search_price_per_call", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}},
{Name: "claude_code_only", Type: field.TypeBool, Default: false},
{Name: "fallback_group_id", Type: field.TypeInt64, Nullable: true},
@@ -985,7 +986,7 @@ var (
{
Name: "group_sort_order",
Unique: false,
Columns: []*schema.Column{GroupsColumns[42]},
Columns: []*schema.Column{GroupsColumns[43]},
},
{
Name: "idx_groups_duplicate_operation_id_active",
+74 -1
View File
@@ -21896,6 +21896,7 @@ type GroupMutation struct {
addvideo_price_720p *float64
video_price_1080p *float64
addvideo_price_1080p *float64
video_model_prices *map[string]map[string]float64
web_search_price_per_call *float64
addweb_search_price_per_call *float64
claude_code_only *bool
@@ -23722,6 +23723,55 @@ func (m *GroupMutation) ResetVideoPrice1080p() {
delete(m.clearedFields, group.FieldVideoPrice1080p)
}
// SetVideoModelPrices sets the "video_model_prices" field.
func (m *GroupMutation) SetVideoModelPrices(value map[string]map[string]float64) {
m.video_model_prices = &value
}
// VideoModelPrices returns the value of the "video_model_prices" field in the mutation.
func (m *GroupMutation) VideoModelPrices() (r map[string]map[string]float64, exists bool) {
v := m.video_model_prices
if v == nil {
return
}
return *v, true
}
// OldVideoModelPrices returns the old "video_model_prices" 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) OldVideoModelPrices(ctx context.Context) (v map[string]map[string]float64, err error) {
if !m.op.Is(OpUpdateOne) {
return v, errors.New("OldVideoModelPrices is only allowed on UpdateOne operations")
}
if m.id == nil || m.oldValue == nil {
return v, errors.New("OldVideoModelPrices requires an ID field in the mutation")
}
oldValue, err := m.oldValue(ctx)
if err != nil {
return v, fmt.Errorf("querying old value for OldVideoModelPrices: %w", err)
}
return oldValue.VideoModelPrices, nil
}
// ClearVideoModelPrices clears the value of the "video_model_prices" field.
func (m *GroupMutation) ClearVideoModelPrices() {
m.video_model_prices = nil
m.clearedFields[group.FieldVideoModelPrices] = struct{}{}
}
// VideoModelPricesCleared returns if the "video_model_prices" field was cleared in this mutation.
func (m *GroupMutation) VideoModelPricesCleared() bool {
_, ok := m.clearedFields[group.FieldVideoModelPrices]
return ok
}
// ResetVideoModelPrices resets all changes to the "video_model_prices" field.
func (m *GroupMutation) ResetVideoModelPrices() {
m.video_model_prices = nil
delete(m.clearedFields, group.FieldVideoModelPrices)
}
// SetWebSearchPricePerCall sets the "web_search_price_per_call" field.
func (m *GroupMutation) SetWebSearchPricePerCall(f float64) {
m.web_search_price_per_call = &f
@@ -25097,7 +25147,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, 55)
fields := make([]string, 0, 56)
if m.created_at != nil {
fields = append(fields, group.FieldCreatedAt)
}
@@ -25197,6 +25247,9 @@ func (m *GroupMutation) Fields() []string {
if m.video_price_1080p != nil {
fields = append(fields, group.FieldVideoPrice1080p)
}
if m.video_model_prices != nil {
fields = append(fields, group.FieldVideoModelPrices)
}
if m.web_search_price_per_call != nil {
fields = append(fields, group.FieldWebSearchPricePerCall)
}
@@ -25337,6 +25390,8 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) {
return m.VideoPrice720p()
case group.FieldVideoPrice1080p:
return m.VideoPrice1080p()
case group.FieldVideoModelPrices:
return m.VideoModelPrices()
case group.FieldWebSearchPricePerCall:
return m.WebSearchPricePerCall()
case group.FieldClaudeCodeOnly:
@@ -25456,6 +25511,8 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e
return m.OldVideoPrice720p(ctx)
case group.FieldVideoPrice1080p:
return m.OldVideoPrice1080p(ctx)
case group.FieldVideoModelPrices:
return m.OldVideoModelPrices(ctx)
case group.FieldWebSearchPricePerCall:
return m.OldWebSearchPricePerCall(ctx)
case group.FieldClaudeCodeOnly:
@@ -25740,6 +25797,13 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error {
}
m.SetVideoPrice1080p(v)
return nil
case group.FieldVideoModelPrices:
v, ok := value.(map[string]map[string]float64)
if !ok {
return fmt.Errorf("unexpected type %T for field %s", value, name)
}
m.SetVideoModelPrices(v)
return nil
case group.FieldWebSearchPricePerCall:
v, ok := value.(float64)
if !ok {
@@ -26239,6 +26303,9 @@ func (m *GroupMutation) ClearedFields() []string {
if m.FieldCleared(group.FieldVideoPrice1080p) {
fields = append(fields, group.FieldVideoPrice1080p)
}
if m.FieldCleared(group.FieldVideoModelPrices) {
fields = append(fields, group.FieldVideoModelPrices)
}
if m.FieldCleared(group.FieldWebSearchPricePerCall) {
fields = append(fields, group.FieldWebSearchPricePerCall)
}
@@ -26301,6 +26368,9 @@ func (m *GroupMutation) ClearField(name string) error {
case group.FieldVideoPrice1080p:
m.ClearVideoPrice1080p()
return nil
case group.FieldVideoModelPrices:
m.ClearVideoModelPrices()
return nil
case group.FieldWebSearchPricePerCall:
m.ClearWebSearchPricePerCall()
return nil
@@ -26420,6 +26490,9 @@ func (m *GroupMutation) ResetField(name string) error {
case group.FieldVideoPrice1080p:
m.ResetVideoPrice1080p()
return nil
case group.FieldVideoModelPrices:
m.ResetVideoModelPrices()
return nil
case group.FieldWebSearchPricePerCall:
m.ResetWebSearchPricePerCall()
return nil
+18 -18
View File
@@ -1118,79 +1118,79 @@ func init() {
// group.DefaultVideoRateMultiplier holds the default value on creation for the video_rate_multiplier field.
group.DefaultVideoRateMultiplier = groupDescVideoRateMultiplier.Default.(float64)
// groupDescClaudeCodeOnly is the schema descriptor for claude_code_only field.
groupDescClaudeCodeOnly := groupFields[31].Descriptor()
groupDescClaudeCodeOnly := groupFields[32].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[35].Descriptor()
groupDescModelRoutingEnabled := groupFields[36].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[36].Descriptor()
groupDescMcpXMLInject := groupFields[37].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[37].Descriptor()
groupDescSupportedModelScopes := groupFields[38].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[38].Descriptor()
groupDescSortOrder := groupFields[39].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[39].Descriptor()
groupDescAllowMessagesDispatch := groupFields[40].Descriptor()
// group.DefaultAllowMessagesDispatch holds the default value on creation for the allow_messages_dispatch field.
group.DefaultAllowMessagesDispatch = groupDescAllowMessagesDispatch.Default.(bool)
// groupDescAllowLive is the schema descriptor for allow_live field.
groupDescAllowLive := groupFields[40].Descriptor()
groupDescAllowLive := groupFields[41].Descriptor()
// group.DefaultAllowLive holds the default value on creation for the allow_live field.
group.DefaultAllowLive = groupDescAllowLive.Default.(bool)
// groupDescRequireOauthOnly is the schema descriptor for require_oauth_only field.
groupDescRequireOauthOnly := groupFields[41].Descriptor()
groupDescRequireOauthOnly := groupFields[42].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[42].Descriptor()
groupDescRequirePrivacySet := groupFields[43].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[43].Descriptor()
groupDescDefaultMappedModel := groupFields[44].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[44].Descriptor()
groupDescMessagesDispatchModelConfig := groupFields[45].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[45].Descriptor()
groupDescModelsListConfig := groupFields[46].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[46].Descriptor()
groupDescRpmLimit := groupFields[47].Descriptor()
// group.DefaultRpmLimit holds the default value on creation for the rpm_limit field.
group.DefaultRpmLimit = groupDescRpmLimit.Default.(int)
// groupDescMaxReasoningEffort is the schema descriptor for max_reasoning_effort field.
groupDescMaxReasoningEffort := groupFields[47].Descriptor()
groupDescMaxReasoningEffort := groupFields[48].Descriptor()
// group.DefaultMaxReasoningEffort holds the default value on creation for the max_reasoning_effort field.
group.DefaultMaxReasoningEffort = groupDescMaxReasoningEffort.Default.(string)
// group.MaxReasoningEffortValidator is a validator for the "max_reasoning_effort" field. It is called by the builders before save.
group.MaxReasoningEffortValidator = groupDescMaxReasoningEffort.Validators[0].(func(string) error)
// groupDescReasoningEffortMappings is the schema descriptor for reasoning_effort_mappings field.
groupDescReasoningEffortMappings := groupFields[48].Descriptor()
groupDescReasoningEffortMappings := groupFields[49].Descriptor()
// group.DefaultReasoningEffortMappings holds the default value on creation for the reasoning_effort_mappings field.
group.DefaultReasoningEffortMappings = groupDescReasoningEffortMappings.Default.([]domain.ReasoningEffortMapping)
// groupDescProfitControlEnabled is the schema descriptor for profit_control_enabled field.
groupDescProfitControlEnabled := groupFields[49].Descriptor()
groupDescProfitControlEnabled := groupFields[50].Descriptor()
// group.DefaultProfitControlEnabled holds the default value on creation for the profit_control_enabled field.
group.DefaultProfitControlEnabled = groupDescProfitControlEnabled.Default.(bool)
// groupDescProfitMinMargin is the schema descriptor for profit_min_margin field.
groupDescProfitMinMargin := groupFields[50].Descriptor()
groupDescProfitMinMargin := groupFields[51].Descriptor()
// group.DefaultProfitMinMargin holds the default value on creation for the profit_min_margin field.
group.DefaultProfitMinMargin = groupDescProfitMinMargin.Default.(float64)
// groupDescProfitSafetyBuffer is the schema descriptor for profit_safety_buffer field.
groupDescProfitSafetyBuffer := groupFields[51].Descriptor()
groupDescProfitSafetyBuffer := groupFields[52].Descriptor()
// group.DefaultProfitSafetyBuffer holds the default value on creation for the profit_safety_buffer field.
group.DefaultProfitSafetyBuffer = groupDescProfitSafetyBuffer.Default.(float64)
idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin()
+4
View File
@@ -148,6 +148,10 @@ func (Group) Fields() []ent.Field {
Optional().
Nillable().
SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}),
field.JSON("video_model_prices", map[string]map[string]float64{}).
Optional().
SchemaType(map[string]string{dialect.Postgres: "jsonb"}).
Comment("按模型族和分辨率覆盖视频每秒价格"),
field.Float("web_search_price_per_call").
Optional().
Nillable().
@@ -969,6 +969,7 @@ func groupEntityToService(g *dbent.Group) *service.Group {
VideoPrice480P: g.VideoPrice480p,
VideoPrice720P: g.VideoPrice720p,
VideoPrice1080P: g.VideoPrice1080p,
VideoModelPrices: service.NormalizeVideoModelPrices(g.VideoModelPrices),
WebSearchPricePerCall: g.WebSearchPricePerCall,
DefaultValidityDays: g.DefaultValidityDays,
ClaudeCodeOnly: g.ClaudeCodeOnly,
@@ -19,6 +19,9 @@ func TestGroupEntityToService_PreservesMessagesDispatchModelConfig(t *testing.T)
RateMultiplier: 1,
AllowMessagesDispatch: true,
DefaultMappedModel: "gpt-5.4",
VideoModelPrices: map[string]map[string]float64{
service.VideoPriceFamilyGrokImagineVideo15: {service.VideoBillingResolution720P: 0.14},
},
MessagesDispatchModelConfig: service.OpenAIMessagesDispatchModelConfig{
OpusMappedModel: "gpt-5.4-nano",
SonnetMappedModel: "gpt-5.3-codex",
@@ -32,6 +35,7 @@ func TestGroupEntityToService_PreservesMessagesDispatchModelConfig(t *testing.T)
got := groupEntityToService(group)
require.NotNil(t, got)
require.Equal(t, group.MessagesDispatchModelConfig, got.MessagesDispatchModelConfig)
require.Equal(t, group.VideoModelPrices, got.VideoModelPrices)
}
func TestAPIKeyRepository_GetByKeyForAuth_PreservesMessagesDispatchModelConfig_SQLite(t *testing.T) {
+3 -11
View File
@@ -46,9 +46,6 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er
if err := createGroupRecord(ctx, r.client, groupIn); err != nil {
return err
}
if saveErr := saveGroupVideoModelPrices(ctx, r.sql, groupIn.ID, groupIn.VideoModelPrices); saveErr != nil {
return fmt.Errorf("save group video_model_prices: %w", saveErr)
}
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventGroupChanged, nil, &groupIn.ID, nil); err != nil {
logger.LegacyPrintf("repository.group", "[SchedulerOutbox] enqueue group create failed: group=%d err=%v", groupIn.ID, err)
}
@@ -85,6 +82,7 @@ func createGroupRecord(ctx context.Context, client *dbent.Client, groupIn *servi
SetNillableVideoPrice480p(groupIn.VideoPrice480P).
SetNillableVideoPrice720p(groupIn.VideoPrice720P).
SetNillableVideoPrice1080p(groupIn.VideoPrice1080P).
SetVideoModelPrices(service.NormalizeVideoModelPrices(groupIn.VideoModelPrices)).
SetNillableWebSearchPricePerCall(groupIn.WebSearchPricePerCall).
SetDefaultValidityDays(groupIn.DefaultValidityDays).
SetClaudeCodeOnly(groupIn.ClaudeCodeOnly).
@@ -228,11 +226,7 @@ func (r *groupRepository) GetByIDLite(ctx context.Context, id int64) (*service.G
if err != nil {
return nil, translatePersistenceError(err, service.ErrGroupNotFound, nil)
}
out := groupEntityToService(m)
if prices, loadErr := loadGroupVideoModelPrices(ctx, r.sql, []int64{id}); loadErr == nil {
applyVideoModelPricesToGroup(out, prices)
}
return out, nil
return groupEntityToService(m), nil
}
func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) error {
@@ -261,6 +255,7 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er
SetNillableVideoPrice480p(groupIn.VideoPrice480P).
SetNillableVideoPrice720p(groupIn.VideoPrice720P).
SetNillableVideoPrice1080p(groupIn.VideoPrice1080P).
SetVideoModelPrices(service.NormalizeVideoModelPrices(groupIn.VideoModelPrices)).
SetDefaultValidityDays(groupIn.DefaultValidityDays).
SetClaudeCodeOnly(groupIn.ClaudeCodeOnly).
SetModelRoutingEnabled(groupIn.ModelRoutingEnabled).
@@ -363,9 +358,6 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er
return translatePersistenceError(err, service.ErrGroupNotFound, service.ErrGroupExists)
}
groupIn.UpdatedAt = updated.UpdatedAt
if err := saveGroupVideoModelPrices(ctx, r.sql, groupIn.ID, groupIn.VideoModelPrices); err != nil {
return fmt.Errorf("save group video_model_prices: %w", err)
}
if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventGroupChanged, nil, &groupIn.ID, nil); err != nil {
logger.LegacyPrintf("repository.group", "[SchedulerOutbox] enqueue group update failed: group=%d err=%v", groupIn.ID, err)
}
@@ -1,114 +0,0 @@
package repository
import (
"context"
"database/sql"
"encoding/json"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/lib/pq"
)
func loadGroupVideoModelPrices(ctx context.Context, sqlq sqlExecutor, groupIDs []int64) (map[int64]map[string]map[string]float64, error) {
out := make(map[int64]map[string]map[string]float64, len(groupIDs))
if sqlq == nil || len(groupIDs) == 0 {
return out, nil
}
rows, err := sqlq.QueryContext(ctx, `
SELECT id, video_model_prices
FROM groups
WHERE id = ANY($1) AND deleted_at IS NULL
`, pq.Array(groupIDs))
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var (
groupID int64
raw []byte
)
if err := rows.Scan(&groupID, &raw); err != nil {
return nil, err
}
prices, err := decodeVideoModelPrices(raw)
if err != nil {
return nil, err
}
if prices != nil {
out[groupID] = prices
}
}
if err := rows.Err(); err != nil {
return nil, err
}
return out, nil
}
func saveGroupVideoModelPrices(ctx context.Context, sqlq sqlExecutor, groupID int64, prices map[string]map[string]float64) error {
if sqlq == nil || groupID <= 0 {
return nil
}
normalized := service.NormalizeVideoModelPrices(prices)
if len(normalized) == 0 {
_, err := sqlq.ExecContext(ctx, `
UPDATE groups
SET video_model_prices = NULL
WHERE id = $1
`, groupID)
return err
}
payload, err := json.Marshal(normalized)
if err != nil {
return err
}
_, err = sqlq.ExecContext(ctx, `
UPDATE groups
SET video_model_prices = $1::jsonb
WHERE id = $2
`, string(payload), groupID)
return err
}
func applyVideoModelPricesToGroups(groups []service.Group, pricesByID map[int64]map[string]map[string]float64) {
for i := range groups {
if prices, ok := pricesByID[groups[i].ID]; ok {
groups[i].VideoModelPrices = service.NormalizeVideoModelPrices(prices)
continue
}
groups[i].VideoModelPrices = service.NormalizeVideoModelPrices(groups[i].VideoModelPrices)
}
}
func applyVideoModelPricesToGroup(group *service.Group, pricesByID map[int64]map[string]map[string]float64) {
if group == nil {
return
}
if prices, ok := pricesByID[group.ID]; ok {
group.VideoModelPrices = service.NormalizeVideoModelPrices(prices)
return
}
group.VideoModelPrices = service.NormalizeVideoModelPrices(group.VideoModelPrices)
}
func decodeVideoModelPrices(raw []byte) (map[string]map[string]float64, error) {
if len(raw) == 0 {
return nil, nil
}
// Driver may return NULL as nil slice; treat empty JSON as nil.
trimmed := string(raw)
if trimmed == "" || trimmed == "null" {
return nil, nil
}
var parsed map[string]map[string]float64
if err := json.Unmarshal(raw, &parsed); err != nil {
// Some drivers surface NULL via sql.NullString paths; tolerate empty object.
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
return service.NormalizeVideoModelPrices(parsed), nil
}
@@ -67,6 +67,21 @@ func cloneGroupModelRouting(value map[string][]int64) map[string][]int64 {
return cloned
}
func cloneGroupVideoModelPrices(value map[string]map[string]float64) map[string]map[string]float64 {
if value == nil {
return nil
}
cloned := make(map[string]map[string]float64, len(value))
for model, prices := range value {
clonedPrices := make(map[string]float64, len(prices))
for resolution, price := range prices {
clonedPrices[resolution] = price
}
cloned[model] = clonedPrices
}
return cloned
}
func cloneGroupMessagesDispatchModelConfig(value OpenAIMessagesDispatchModelConfig) OpenAIMessagesDispatchModelConfig {
cloned := value
if value.ExactModelMappings != nil {
@@ -113,6 +128,7 @@ func cloneGroupForDuplicate(source *Group, operationID string) *Group {
VideoPrice480P: cloneGroupValuePointer(source.VideoPrice480P),
VideoPrice720P: cloneGroupValuePointer(source.VideoPrice720P),
VideoPrice1080P: cloneGroupValuePointer(source.VideoPrice1080P),
VideoModelPrices: cloneGroupVideoModelPrices(source.VideoModelPrices),
WebSearchPricePerCall: cloneGroupValuePointer(source.WebSearchPricePerCall),
ClaudeCodeOnly: source.ClaudeCodeOnly,
FallbackGroupID: cloneGroupValuePointer(source.FallbackGroupID),
@@ -121,37 +121,40 @@ func groupDuplicateTestPointer[T any](value T) *T { return &value }
func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing.T) {
createdAt := time.Date(2026, time.July, 1, 2, 3, 4, 0, time.UTC)
source := &Group{
ID: 41,
Name: "高级订阅",
Description: "configuration",
Platform: PlatformOpenAI,
RateMultiplier: 1.75,
PeakRateEnabled: true,
PeakStart: "09:00",
PeakEnd: "18:00",
PeakRateMultiplier: 1.2,
IsExclusive: true,
Status: StatusActive,
Hydrated: true,
SubscriptionType: SubscriptionTypeSubscription,
DailyLimitUSD: groupDuplicateTestPointer(11.0),
WeeklyLimitUSD: groupDuplicateTestPointer(22.0),
MonthlyLimitUSD: groupDuplicateTestPointer(33.0),
DefaultValidityDays: 91,
AllowImageGeneration: true,
AllowBatchImageGeneration: true,
ImageRateIndependent: true,
ImageRateMultiplier: 1.4,
ImagePrice1K: groupDuplicateTestPointer(0.01),
ImagePrice2K: groupDuplicateTestPointer(0.02),
ImagePrice4K: groupDuplicateTestPointer(0.04),
BatchImageDiscountMultiplier: 0.4,
BatchImageHoldMultiplier: 0.7,
VideoRateIndependent: true,
VideoRateMultiplier: 2.1,
VideoPrice480P: groupDuplicateTestPointer(0.1),
VideoPrice720P: groupDuplicateTestPointer(0.2),
VideoPrice1080P: groupDuplicateTestPointer(0.3),
ID: 41,
Name: "高级订阅",
Description: "configuration",
Platform: PlatformOpenAI,
RateMultiplier: 1.75,
PeakRateEnabled: true,
PeakStart: "09:00",
PeakEnd: "18:00",
PeakRateMultiplier: 1.2,
IsExclusive: true,
Status: StatusActive,
Hydrated: true,
SubscriptionType: SubscriptionTypeSubscription,
DailyLimitUSD: groupDuplicateTestPointer(11.0),
WeeklyLimitUSD: groupDuplicateTestPointer(22.0),
MonthlyLimitUSD: groupDuplicateTestPointer(33.0),
DefaultValidityDays: 91,
AllowImageGeneration: true,
AllowBatchImageGeneration: true,
ImageRateIndependent: true,
ImageRateMultiplier: 1.4,
ImagePrice1K: groupDuplicateTestPointer(0.01),
ImagePrice2K: groupDuplicateTestPointer(0.02),
ImagePrice4K: groupDuplicateTestPointer(0.04),
BatchImageDiscountMultiplier: 0.4,
BatchImageHoldMultiplier: 0.7,
VideoRateIndependent: true,
VideoRateMultiplier: 2.1,
VideoPrice480P: groupDuplicateTestPointer(0.1),
VideoPrice720P: groupDuplicateTestPointer(0.2),
VideoPrice1080P: groupDuplicateTestPointer(0.3),
VideoModelPrices: map[string]map[string]float64{
VideoPriceFamilyGrokImagineVideo15: {VideoBillingResolution720P: 0.14},
},
WebSearchPricePerCall: groupDuplicateTestPointer(0.005),
ClaudeCodeOnly: true,
FallbackGroupID: groupDuplicateTestPointer(int64(7)),
@@ -204,6 +207,7 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
require.Equal(t, source.PeakRateMultiplier, duplicate.PeakRateMultiplier)
require.Equal(t, source.DefaultValidityDays, duplicate.DefaultValidityDays)
require.Equal(t, source.ImagePrice4K, duplicate.ImagePrice4K)
require.Equal(t, source.VideoModelPrices, duplicate.VideoModelPrices)
require.Equal(t, source.WebSearchPricePerCall, duplicate.WebSearchPricePerCall)
require.Equal(t, source.FallbackGroupID, duplicate.FallbackGroupID)
require.Equal(t, source.ModelRouting, duplicate.ModelRouting)
@@ -222,12 +226,14 @@ func TestDuplicateGroupCopiesConfigurationDeeplyAndResetsRuntimeState(t *testing
}, repo.createdBindings[duplicate.ID])
duplicate.ModelRouting["gpt-*"][0] = 999
duplicate.VideoModelPrices[VideoPriceFamilyGrokImagineVideo15][VideoBillingResolution720P] = 999
duplicate.SupportedModelScopes[0] = "changed"
duplicate.MessagesDispatchModelConfig.ExactModelMappings["claude-special"] = "changed"
duplicate.ModelsListConfig.Models[0] = "changed"
duplicate.ReasoningEffortMappings[0].To = "changed"
*duplicate.DailyLimitUSD = 999
require.Equal(t, int64(13), source.ModelRouting["gpt-*"][0])
require.Equal(t, 0.14, source.VideoModelPrices[VideoPriceFamilyGrokImagineVideo15][VideoBillingResolution720P])
require.Equal(t, "claude", source.SupportedModelScopes[0])
require.Equal(t, "gpt-special", source.MessagesDispatchModelConfig.ExactModelMappings["claude-special"])
require.Equal(t, "gpt-5.4", source.ModelsListConfig.Models[0])