From d4c2b723a58058093d0c23b267ba6e1c3ada462c Mon Sep 17 00:00:00 2001 From: song Date: Mon, 5 Jan 2026 17:07:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=9B=BE=E7=89=87=E7=94=9F=E6=88=90?= =?UTF-8?q?=E8=AE=A1=E8=B4=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 Group 图片价格配置(image_price_1k/2k/4k) - BillingService 新增 CalculateImageCost 方法 - AntigravityGatewayService 支持识别图片生成模型并按次计费 - UsageLog 新增 image_count 和 image_size 字段 - 前端分组管理支持配置图片价格(antigravity 和 gemini 平台) - 图片计费复用通用计费能力(余额检查、扣费、倍率、订阅限额) --- backend/ent/group.go | 44 +- backend/ent/group/group.go | 24 + backend/ent/group/where.go | 165 ++++++ backend/ent/group_create.go | 294 +++++++++++ backend/ent/group_update.go | 216 ++++++++ backend/ent/migrate/schema.go | 31 +- backend/ent/mutation.go | 485 +++++++++++++++++- backend/ent/runtime/runtime.go | 10 +- backend/ent/schema/group.go | 14 + backend/ent/schema/usage_log.go | 8 + backend/ent/usagelog.go | 29 +- backend/ent/usagelog/usagelog.go | 20 + backend/ent/usagelog/where.go | 125 +++++ backend/ent/usagelog_create.go | 168 ++++++ backend/ent/usagelog_update.go | 116 +++++ .../internal/handler/admin/group_handler.go | 14 + backend/internal/handler/dto/mappers.go | 5 + backend/internal/handler/dto/types.go | 9 + .../internal/pkg/antigravity/gemini_types.go | 7 + backend/internal/repository/api_key_repo.go | 3 + backend/internal/repository/group_repo.go | 6 + backend/internal/repository/usage_log_repo.go | 25 +- backend/internal/service/admin_service.go | 21 + .../service/admin_service_group_test.go | 197 +++++++ .../service/antigravity_gateway_service.go | 36 ++ .../service/antigravity_image_test.go | 120 +++++ backend/internal/service/billing_service.go | 85 +++ .../service/billing_service_image_test.go | 149 ++++++ backend/internal/service/gateway_service.go | 51 +- backend/internal/service/group.go | 21 + backend/internal/service/group_test.go | 92 ++++ backend/internal/service/pricing_service.go | 5 + backend/internal/service/usage_log.go | 4 + .../migrations/028_group_image_pricing.sql | 10 + .../migrations/029_usage_log_image_fields.sql | 5 + frontend/src/i18n/locales/en.ts | 7 +- frontend/src/i18n/locales/zh.ts | 7 +- frontend/src/types/index.ts | 9 + frontend/src/views/admin/GroupsView.vue | 108 +++- frontend/src/views/admin/UsageView.vue | 21 +- frontend/src/views/user/UsageView.vue | 21 +- 41 files changed, 2747 insertions(+), 40 deletions(-) create mode 100644 backend/internal/service/admin_service_group_test.go create mode 100644 backend/internal/service/antigravity_image_test.go create mode 100644 backend/internal/service/billing_service_image_test.go create mode 100644 backend/internal/service/group_test.go create mode 100644 backend/migrations/028_group_image_pricing.sql create mode 100644 backend/migrations/029_usage_log_image_fields.sql diff --git a/backend/ent/group.go b/backend/ent/group.go index e8687224..dca64cec 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -45,6 +45,12 @@ type Group struct { MonthlyLimitUsd *float64 `json:"monthly_limit_usd,omitempty"` // DefaultValidityDays holds the value of the "default_validity_days" field. DefaultValidityDays int `json:"default_validity_days,omitempty"` + // ImagePrice1k holds the value of the "image_price_1k" field. + ImagePrice1k *float64 `json:"image_price_1k,omitempty"` + // ImagePrice2k holds the value of the "image_price_2k" field. + ImagePrice2k *float64 `json:"image_price_2k,omitempty"` + // ImagePrice4k holds the value of the "image_price_4k" field. + ImagePrice4k *float64 `json:"image_price_4k,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the GroupQuery when eager-loading is set. Edges GroupEdges `json:"edges"` @@ -153,7 +159,7 @@ func (*Group) scanValues(columns []string) ([]any, error) { switch columns[i] { case group.FieldIsExclusive: values[i] = new(sql.NullBool) - case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd: + case group.FieldRateMultiplier, group.FieldDailyLimitUsd, group.FieldWeeklyLimitUsd, group.FieldMonthlyLimitUsd, group.FieldImagePrice1k, group.FieldImagePrice2k, group.FieldImagePrice4k: values[i] = new(sql.NullFloat64) case group.FieldID, group.FieldDefaultValidityDays: values[i] = new(sql.NullInt64) @@ -271,6 +277,27 @@ func (_m *Group) assignValues(columns []string, values []any) error { } else if value.Valid { _m.DefaultValidityDays = int(value.Int64) } + case group.FieldImagePrice1k: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field image_price_1k", values[i]) + } else if value.Valid { + _m.ImagePrice1k = new(float64) + *_m.ImagePrice1k = value.Float64 + } + case group.FieldImagePrice2k: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field image_price_2k", values[i]) + } else if value.Valid { + _m.ImagePrice2k = new(float64) + *_m.ImagePrice2k = value.Float64 + } + case group.FieldImagePrice4k: + if value, ok := values[i].(*sql.NullFloat64); !ok { + return fmt.Errorf("unexpected type %T for field image_price_4k", values[i]) + } else if value.Valid { + _m.ImagePrice4k = new(float64) + *_m.ImagePrice4k = value.Float64 + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -398,6 +425,21 @@ func (_m *Group) String() string { builder.WriteString(", ") builder.WriteString("default_validity_days=") builder.WriteString(fmt.Sprintf("%v", _m.DefaultValidityDays)) + builder.WriteString(", ") + if v := _m.ImagePrice1k; v != nil { + builder.WriteString("image_price_1k=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + if v := _m.ImagePrice2k; v != nil { + builder.WriteString("image_price_2k=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + if v := _m.ImagePrice4k; v != nil { + builder.WriteString("image_price_4k=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index 1934b17b..1c5ed343 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -43,6 +43,12 @@ const ( FieldMonthlyLimitUsd = "monthly_limit_usd" // FieldDefaultValidityDays holds the string denoting the default_validity_days field in the database. FieldDefaultValidityDays = "default_validity_days" + // FieldImagePrice1k holds the string denoting the image_price_1k field in the database. + FieldImagePrice1k = "image_price_1k" + // FieldImagePrice2k holds the string denoting the image_price_2k field in the database. + FieldImagePrice2k = "image_price_2k" + // FieldImagePrice4k holds the string denoting the image_price_4k field in the database. + FieldImagePrice4k = "image_price_4k" // EdgeAPIKeys holds the string denoting the api_keys edge name in mutations. EdgeAPIKeys = "api_keys" // EdgeRedeemCodes holds the string denoting the redeem_codes edge name in mutations. @@ -132,6 +138,9 @@ var Columns = []string{ FieldWeeklyLimitUsd, FieldMonthlyLimitUsd, FieldDefaultValidityDays, + FieldImagePrice1k, + FieldImagePrice2k, + FieldImagePrice4k, } var ( @@ -267,6 +276,21 @@ func ByDefaultValidityDays(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDefaultValidityDays, opts...).ToFunc() } +// ByImagePrice1k orders the results by the image_price_1k field. +func ByImagePrice1k(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldImagePrice1k, opts...).ToFunc() +} + +// ByImagePrice2k orders the results by the image_price_2k field. +func ByImagePrice2k(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldImagePrice2k, opts...).ToFunc() +} + +// ByImagePrice4k orders the results by the image_price_4k field. +func ByImagePrice4k(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldImagePrice4k, opts...).ToFunc() +} + // ByAPIKeysCount orders the results by api_keys count. func ByAPIKeysCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { diff --git a/backend/ent/group/where.go b/backend/ent/group/where.go index cb553242..7bce1fe6 100644 --- a/backend/ent/group/where.go +++ b/backend/ent/group/where.go @@ -125,6 +125,21 @@ func DefaultValidityDays(v int) predicate.Group { return predicate.Group(sql.FieldEQ(FieldDefaultValidityDays, v)) } +// ImagePrice1k applies equality check predicate on the "image_price_1k" field. It's identical to ImagePrice1kEQ. +func ImagePrice1k(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldImagePrice1k, v)) +} + +// ImagePrice2k applies equality check predicate on the "image_price_2k" field. It's identical to ImagePrice2kEQ. +func ImagePrice2k(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldImagePrice2k, v)) +} + +// ImagePrice4k applies equality check predicate on the "image_price_4k" field. It's identical to ImagePrice4kEQ. +func ImagePrice4k(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.Group { return predicate.Group(sql.FieldEQ(FieldCreatedAt, v)) @@ -830,6 +845,156 @@ func DefaultValidityDaysLTE(v int) predicate.Group { return predicate.Group(sql.FieldLTE(FieldDefaultValidityDays, v)) } +// ImagePrice1kEQ applies the EQ predicate on the "image_price_1k" field. +func ImagePrice1kEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldImagePrice1k, v)) +} + +// ImagePrice1kNEQ applies the NEQ predicate on the "image_price_1k" field. +func ImagePrice1kNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldImagePrice1k, v)) +} + +// ImagePrice1kIn applies the In predicate on the "image_price_1k" field. +func ImagePrice1kIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldImagePrice1k, vs...)) +} + +// ImagePrice1kNotIn applies the NotIn predicate on the "image_price_1k" field. +func ImagePrice1kNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldImagePrice1k, vs...)) +} + +// ImagePrice1kGT applies the GT predicate on the "image_price_1k" field. +func ImagePrice1kGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldImagePrice1k, v)) +} + +// ImagePrice1kGTE applies the GTE predicate on the "image_price_1k" field. +func ImagePrice1kGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldImagePrice1k, v)) +} + +// ImagePrice1kLT applies the LT predicate on the "image_price_1k" field. +func ImagePrice1kLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldImagePrice1k, v)) +} + +// ImagePrice1kLTE applies the LTE predicate on the "image_price_1k" field. +func ImagePrice1kLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldImagePrice1k, v)) +} + +// ImagePrice1kIsNil applies the IsNil predicate on the "image_price_1k" field. +func ImagePrice1kIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldImagePrice1k)) +} + +// ImagePrice1kNotNil applies the NotNil predicate on the "image_price_1k" field. +func ImagePrice1kNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldImagePrice1k)) +} + +// ImagePrice2kEQ applies the EQ predicate on the "image_price_2k" field. +func ImagePrice2kEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldImagePrice2k, v)) +} + +// ImagePrice2kNEQ applies the NEQ predicate on the "image_price_2k" field. +func ImagePrice2kNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldImagePrice2k, v)) +} + +// ImagePrice2kIn applies the In predicate on the "image_price_2k" field. +func ImagePrice2kIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldImagePrice2k, vs...)) +} + +// ImagePrice2kNotIn applies the NotIn predicate on the "image_price_2k" field. +func ImagePrice2kNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldImagePrice2k, vs...)) +} + +// ImagePrice2kGT applies the GT predicate on the "image_price_2k" field. +func ImagePrice2kGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldImagePrice2k, v)) +} + +// ImagePrice2kGTE applies the GTE predicate on the "image_price_2k" field. +func ImagePrice2kGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldImagePrice2k, v)) +} + +// ImagePrice2kLT applies the LT predicate on the "image_price_2k" field. +func ImagePrice2kLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldImagePrice2k, v)) +} + +// ImagePrice2kLTE applies the LTE predicate on the "image_price_2k" field. +func ImagePrice2kLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldImagePrice2k, v)) +} + +// ImagePrice2kIsNil applies the IsNil predicate on the "image_price_2k" field. +func ImagePrice2kIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldImagePrice2k)) +} + +// ImagePrice2kNotNil applies the NotNil predicate on the "image_price_2k" field. +func ImagePrice2kNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldImagePrice2k)) +} + +// ImagePrice4kEQ applies the EQ predicate on the "image_price_4k" field. +func ImagePrice4kEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldEQ(FieldImagePrice4k, v)) +} + +// ImagePrice4kNEQ applies the NEQ predicate on the "image_price_4k" field. +func ImagePrice4kNEQ(v float64) predicate.Group { + return predicate.Group(sql.FieldNEQ(FieldImagePrice4k, v)) +} + +// ImagePrice4kIn applies the In predicate on the "image_price_4k" field. +func ImagePrice4kIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldIn(FieldImagePrice4k, vs...)) +} + +// ImagePrice4kNotIn applies the NotIn predicate on the "image_price_4k" field. +func ImagePrice4kNotIn(vs ...float64) predicate.Group { + return predicate.Group(sql.FieldNotIn(FieldImagePrice4k, vs...)) +} + +// ImagePrice4kGT applies the GT predicate on the "image_price_4k" field. +func ImagePrice4kGT(v float64) predicate.Group { + return predicate.Group(sql.FieldGT(FieldImagePrice4k, v)) +} + +// ImagePrice4kGTE applies the GTE predicate on the "image_price_4k" field. +func ImagePrice4kGTE(v float64) predicate.Group { + return predicate.Group(sql.FieldGTE(FieldImagePrice4k, v)) +} + +// ImagePrice4kLT applies the LT predicate on the "image_price_4k" field. +func ImagePrice4kLT(v float64) predicate.Group { + return predicate.Group(sql.FieldLT(FieldImagePrice4k, v)) +} + +// ImagePrice4kLTE applies the LTE predicate on the "image_price_4k" field. +func ImagePrice4kLTE(v float64) predicate.Group { + return predicate.Group(sql.FieldLTE(FieldImagePrice4k, v)) +} + +// ImagePrice4kIsNil applies the IsNil predicate on the "image_price_4k" field. +func ImagePrice4kIsNil() predicate.Group { + return predicate.Group(sql.FieldIsNull(FieldImagePrice4k)) +} + +// ImagePrice4kNotNil applies the NotNil predicate on the "image_price_4k" field. +func ImagePrice4kNotNil() predicate.Group { + return predicate.Group(sql.FieldNotNull(FieldImagePrice4k)) +} + // HasAPIKeys applies the HasEdge predicate on the "api_keys" edge. func HasAPIKeys() predicate.Group { return predicate.Group(func(s *sql.Selector) { diff --git a/backend/ent/group_create.go b/backend/ent/group_create.go index 0613c78e..6a928af6 100644 --- a/backend/ent/group_create.go +++ b/backend/ent/group_create.go @@ -216,6 +216,48 @@ func (_c *GroupCreate) SetNillableDefaultValidityDays(v *int) *GroupCreate { return _c } +// SetImagePrice1k sets the "image_price_1k" field. +func (_c *GroupCreate) SetImagePrice1k(v float64) *GroupCreate { + _c.mutation.SetImagePrice1k(v) + return _c +} + +// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil. +func (_c *GroupCreate) SetNillableImagePrice1k(v *float64) *GroupCreate { + if v != nil { + _c.SetImagePrice1k(*v) + } + return _c +} + +// SetImagePrice2k sets the "image_price_2k" field. +func (_c *GroupCreate) SetImagePrice2k(v float64) *GroupCreate { + _c.mutation.SetImagePrice2k(v) + return _c +} + +// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil. +func (_c *GroupCreate) SetNillableImagePrice2k(v *float64) *GroupCreate { + if v != nil { + _c.SetImagePrice2k(*v) + } + return _c +} + +// SetImagePrice4k sets the "image_price_4k" field. +func (_c *GroupCreate) SetImagePrice4k(v float64) *GroupCreate { + _c.mutation.SetImagePrice4k(v) + return _c +} + +// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil. +func (_c *GroupCreate) SetNillableImagePrice4k(v *float64) *GroupCreate { + if v != nil { + _c.SetImagePrice4k(*v) + } + return _c +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_c *GroupCreate) AddAPIKeyIDs(ids ...int64) *GroupCreate { _c.mutation.AddAPIKeyIDs(ids...) @@ -516,6 +558,18 @@ func (_c *GroupCreate) createSpec() (*Group, *sqlgraph.CreateSpec) { _spec.SetField(group.FieldDefaultValidityDays, field.TypeInt, value) _node.DefaultValidityDays = value } + if value, ok := _c.mutation.ImagePrice1k(); ok { + _spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value) + _node.ImagePrice1k = &value + } + if value, ok := _c.mutation.ImagePrice2k(); ok { + _spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value) + _node.ImagePrice2k = &value + } + if value, ok := _c.mutation.ImagePrice4k(); ok { + _spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value) + _node.ImagePrice4k = &value + } if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -888,6 +942,78 @@ func (u *GroupUpsert) AddDefaultValidityDays(v int) *GroupUpsert { return u } +// SetImagePrice1k sets the "image_price_1k" field. +func (u *GroupUpsert) SetImagePrice1k(v float64) *GroupUpsert { + u.Set(group.FieldImagePrice1k, v) + return u +} + +// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create. +func (u *GroupUpsert) UpdateImagePrice1k() *GroupUpsert { + u.SetExcluded(group.FieldImagePrice1k) + return u +} + +// AddImagePrice1k adds v to the "image_price_1k" field. +func (u *GroupUpsert) AddImagePrice1k(v float64) *GroupUpsert { + u.Add(group.FieldImagePrice1k, v) + return u +} + +// ClearImagePrice1k clears the value of the "image_price_1k" field. +func (u *GroupUpsert) ClearImagePrice1k() *GroupUpsert { + u.SetNull(group.FieldImagePrice1k) + return u +} + +// SetImagePrice2k sets the "image_price_2k" field. +func (u *GroupUpsert) SetImagePrice2k(v float64) *GroupUpsert { + u.Set(group.FieldImagePrice2k, v) + return u +} + +// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create. +func (u *GroupUpsert) UpdateImagePrice2k() *GroupUpsert { + u.SetExcluded(group.FieldImagePrice2k) + return u +} + +// AddImagePrice2k adds v to the "image_price_2k" field. +func (u *GroupUpsert) AddImagePrice2k(v float64) *GroupUpsert { + u.Add(group.FieldImagePrice2k, v) + return u +} + +// ClearImagePrice2k clears the value of the "image_price_2k" field. +func (u *GroupUpsert) ClearImagePrice2k() *GroupUpsert { + u.SetNull(group.FieldImagePrice2k) + return u +} + +// SetImagePrice4k sets the "image_price_4k" field. +func (u *GroupUpsert) SetImagePrice4k(v float64) *GroupUpsert { + u.Set(group.FieldImagePrice4k, v) + return u +} + +// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create. +func (u *GroupUpsert) UpdateImagePrice4k() *GroupUpsert { + u.SetExcluded(group.FieldImagePrice4k) + return u +} + +// AddImagePrice4k adds v to the "image_price_4k" field. +func (u *GroupUpsert) AddImagePrice4k(v float64) *GroupUpsert { + u.Add(group.FieldImagePrice4k, v) + return u +} + +// ClearImagePrice4k clears the value of the "image_price_4k" field. +func (u *GroupUpsert) ClearImagePrice4k() *GroupUpsert { + u.SetNull(group.FieldImagePrice4k) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -1185,6 +1311,90 @@ func (u *GroupUpsertOne) UpdateDefaultValidityDays() *GroupUpsertOne { }) } +// SetImagePrice1k sets the "image_price_1k" field. +func (u *GroupUpsertOne) SetImagePrice1k(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetImagePrice1k(v) + }) +} + +// AddImagePrice1k adds v to the "image_price_1k" field. +func (u *GroupUpsertOne) AddImagePrice1k(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddImagePrice1k(v) + }) +} + +// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateImagePrice1k() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateImagePrice1k() + }) +} + +// ClearImagePrice1k clears the value of the "image_price_1k" field. +func (u *GroupUpsertOne) ClearImagePrice1k() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearImagePrice1k() + }) +} + +// SetImagePrice2k sets the "image_price_2k" field. +func (u *GroupUpsertOne) SetImagePrice2k(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetImagePrice2k(v) + }) +} + +// AddImagePrice2k adds v to the "image_price_2k" field. +func (u *GroupUpsertOne) AddImagePrice2k(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddImagePrice2k(v) + }) +} + +// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateImagePrice2k() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateImagePrice2k() + }) +} + +// ClearImagePrice2k clears the value of the "image_price_2k" field. +func (u *GroupUpsertOne) ClearImagePrice2k() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearImagePrice2k() + }) +} + +// SetImagePrice4k sets the "image_price_4k" field. +func (u *GroupUpsertOne) SetImagePrice4k(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.SetImagePrice4k(v) + }) +} + +// AddImagePrice4k adds v to the "image_price_4k" field. +func (u *GroupUpsertOne) AddImagePrice4k(v float64) *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.AddImagePrice4k(v) + }) +} + +// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create. +func (u *GroupUpsertOne) UpdateImagePrice4k() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.UpdateImagePrice4k() + }) +} + +// ClearImagePrice4k clears the value of the "image_price_4k" field. +func (u *GroupUpsertOne) ClearImagePrice4k() *GroupUpsertOne { + return u.Update(func(s *GroupUpsert) { + s.ClearImagePrice4k() + }) +} + // Exec executes the query. func (u *GroupUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -1648,6 +1858,90 @@ func (u *GroupUpsertBulk) UpdateDefaultValidityDays() *GroupUpsertBulk { }) } +// SetImagePrice1k sets the "image_price_1k" field. +func (u *GroupUpsertBulk) SetImagePrice1k(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetImagePrice1k(v) + }) +} + +// AddImagePrice1k adds v to the "image_price_1k" field. +func (u *GroupUpsertBulk) AddImagePrice1k(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddImagePrice1k(v) + }) +} + +// UpdateImagePrice1k sets the "image_price_1k" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateImagePrice1k() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateImagePrice1k() + }) +} + +// ClearImagePrice1k clears the value of the "image_price_1k" field. +func (u *GroupUpsertBulk) ClearImagePrice1k() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearImagePrice1k() + }) +} + +// SetImagePrice2k sets the "image_price_2k" field. +func (u *GroupUpsertBulk) SetImagePrice2k(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetImagePrice2k(v) + }) +} + +// AddImagePrice2k adds v to the "image_price_2k" field. +func (u *GroupUpsertBulk) AddImagePrice2k(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddImagePrice2k(v) + }) +} + +// UpdateImagePrice2k sets the "image_price_2k" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateImagePrice2k() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateImagePrice2k() + }) +} + +// ClearImagePrice2k clears the value of the "image_price_2k" field. +func (u *GroupUpsertBulk) ClearImagePrice2k() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearImagePrice2k() + }) +} + +// SetImagePrice4k sets the "image_price_4k" field. +func (u *GroupUpsertBulk) SetImagePrice4k(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.SetImagePrice4k(v) + }) +} + +// AddImagePrice4k adds v to the "image_price_4k" field. +func (u *GroupUpsertBulk) AddImagePrice4k(v float64) *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.AddImagePrice4k(v) + }) +} + +// UpdateImagePrice4k sets the "image_price_4k" field to the value that was provided on create. +func (u *GroupUpsertBulk) UpdateImagePrice4k() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.UpdateImagePrice4k() + }) +} + +// ClearImagePrice4k clears the value of the "image_price_4k" field. +func (u *GroupUpsertBulk) ClearImagePrice4k() *GroupUpsertBulk { + return u.Update(func(s *GroupUpsert) { + s.ClearImagePrice4k() + }) +} + // Exec executes the query. func (u *GroupUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/backend/ent/group_update.go b/backend/ent/group_update.go index 43dcf319..43555ce2 100644 --- a/backend/ent/group_update.go +++ b/backend/ent/group_update.go @@ -273,6 +273,87 @@ func (_u *GroupUpdate) AddDefaultValidityDays(v int) *GroupUpdate { return _u } +// SetImagePrice1k sets the "image_price_1k" field. +func (_u *GroupUpdate) SetImagePrice1k(v float64) *GroupUpdate { + _u.mutation.ResetImagePrice1k() + _u.mutation.SetImagePrice1k(v) + return _u +} + +// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableImagePrice1k(v *float64) *GroupUpdate { + if v != nil { + _u.SetImagePrice1k(*v) + } + return _u +} + +// AddImagePrice1k adds value to the "image_price_1k" field. +func (_u *GroupUpdate) AddImagePrice1k(v float64) *GroupUpdate { + _u.mutation.AddImagePrice1k(v) + return _u +} + +// ClearImagePrice1k clears the value of the "image_price_1k" field. +func (_u *GroupUpdate) ClearImagePrice1k() *GroupUpdate { + _u.mutation.ClearImagePrice1k() + return _u +} + +// SetImagePrice2k sets the "image_price_2k" field. +func (_u *GroupUpdate) SetImagePrice2k(v float64) *GroupUpdate { + _u.mutation.ResetImagePrice2k() + _u.mutation.SetImagePrice2k(v) + return _u +} + +// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableImagePrice2k(v *float64) *GroupUpdate { + if v != nil { + _u.SetImagePrice2k(*v) + } + return _u +} + +// AddImagePrice2k adds value to the "image_price_2k" field. +func (_u *GroupUpdate) AddImagePrice2k(v float64) *GroupUpdate { + _u.mutation.AddImagePrice2k(v) + return _u +} + +// ClearImagePrice2k clears the value of the "image_price_2k" field. +func (_u *GroupUpdate) ClearImagePrice2k() *GroupUpdate { + _u.mutation.ClearImagePrice2k() + return _u +} + +// SetImagePrice4k sets the "image_price_4k" field. +func (_u *GroupUpdate) SetImagePrice4k(v float64) *GroupUpdate { + _u.mutation.ResetImagePrice4k() + _u.mutation.SetImagePrice4k(v) + return _u +} + +// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil. +func (_u *GroupUpdate) SetNillableImagePrice4k(v *float64) *GroupUpdate { + if v != nil { + _u.SetImagePrice4k(*v) + } + return _u +} + +// AddImagePrice4k adds value to the "image_price_4k" field. +func (_u *GroupUpdate) AddImagePrice4k(v float64) *GroupUpdate { + _u.mutation.AddImagePrice4k(v) + return _u +} + +// ClearImagePrice4k clears the value of the "image_price_4k" field. +func (_u *GroupUpdate) ClearImagePrice4k() *GroupUpdate { + _u.mutation.ClearImagePrice4k() + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *GroupUpdate) AddAPIKeyIDs(ids ...int64) *GroupUpdate { _u.mutation.AddAPIKeyIDs(ids...) @@ -642,6 +723,33 @@ func (_u *GroupUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.AddedDefaultValidityDays(); ok { _spec.AddField(group.FieldDefaultValidityDays, field.TypeInt, value) } + if value, ok := _u.mutation.ImagePrice1k(); ok { + _spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedImagePrice1k(); ok { + _spec.AddField(group.FieldImagePrice1k, field.TypeFloat64, value) + } + if _u.mutation.ImagePrice1kCleared() { + _spec.ClearField(group.FieldImagePrice1k, field.TypeFloat64) + } + if value, ok := _u.mutation.ImagePrice2k(); ok { + _spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedImagePrice2k(); ok { + _spec.AddField(group.FieldImagePrice2k, field.TypeFloat64, value) + } + if _u.mutation.ImagePrice2kCleared() { + _spec.ClearField(group.FieldImagePrice2k, field.TypeFloat64) + } + if value, ok := _u.mutation.ImagePrice4k(); ok { + _spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedImagePrice4k(); ok { + _spec.AddField(group.FieldImagePrice4k, field.TypeFloat64, value) + } + if _u.mutation.ImagePrice4kCleared() { + _spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1195,6 +1303,87 @@ func (_u *GroupUpdateOne) AddDefaultValidityDays(v int) *GroupUpdateOne { return _u } +// SetImagePrice1k sets the "image_price_1k" field. +func (_u *GroupUpdateOne) SetImagePrice1k(v float64) *GroupUpdateOne { + _u.mutation.ResetImagePrice1k() + _u.mutation.SetImagePrice1k(v) + return _u +} + +// SetNillableImagePrice1k sets the "image_price_1k" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableImagePrice1k(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetImagePrice1k(*v) + } + return _u +} + +// AddImagePrice1k adds value to the "image_price_1k" field. +func (_u *GroupUpdateOne) AddImagePrice1k(v float64) *GroupUpdateOne { + _u.mutation.AddImagePrice1k(v) + return _u +} + +// ClearImagePrice1k clears the value of the "image_price_1k" field. +func (_u *GroupUpdateOne) ClearImagePrice1k() *GroupUpdateOne { + _u.mutation.ClearImagePrice1k() + return _u +} + +// SetImagePrice2k sets the "image_price_2k" field. +func (_u *GroupUpdateOne) SetImagePrice2k(v float64) *GroupUpdateOne { + _u.mutation.ResetImagePrice2k() + _u.mutation.SetImagePrice2k(v) + return _u +} + +// SetNillableImagePrice2k sets the "image_price_2k" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableImagePrice2k(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetImagePrice2k(*v) + } + return _u +} + +// AddImagePrice2k adds value to the "image_price_2k" field. +func (_u *GroupUpdateOne) AddImagePrice2k(v float64) *GroupUpdateOne { + _u.mutation.AddImagePrice2k(v) + return _u +} + +// ClearImagePrice2k clears the value of the "image_price_2k" field. +func (_u *GroupUpdateOne) ClearImagePrice2k() *GroupUpdateOne { + _u.mutation.ClearImagePrice2k() + return _u +} + +// SetImagePrice4k sets the "image_price_4k" field. +func (_u *GroupUpdateOne) SetImagePrice4k(v float64) *GroupUpdateOne { + _u.mutation.ResetImagePrice4k() + _u.mutation.SetImagePrice4k(v) + return _u +} + +// SetNillableImagePrice4k sets the "image_price_4k" field if the given value is not nil. +func (_u *GroupUpdateOne) SetNillableImagePrice4k(v *float64) *GroupUpdateOne { + if v != nil { + _u.SetImagePrice4k(*v) + } + return _u +} + +// AddImagePrice4k adds value to the "image_price_4k" field. +func (_u *GroupUpdateOne) AddImagePrice4k(v float64) *GroupUpdateOne { + _u.mutation.AddImagePrice4k(v) + return _u +} + +// ClearImagePrice4k clears the value of the "image_price_4k" field. +func (_u *GroupUpdateOne) ClearImagePrice4k() *GroupUpdateOne { + _u.mutation.ClearImagePrice4k() + return _u +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *GroupUpdateOne) AddAPIKeyIDs(ids ...int64) *GroupUpdateOne { _u.mutation.AddAPIKeyIDs(ids...) @@ -1594,6 +1783,33 @@ func (_u *GroupUpdateOne) sqlSave(ctx context.Context) (_node *Group, err error) if value, ok := _u.mutation.AddedDefaultValidityDays(); ok { _spec.AddField(group.FieldDefaultValidityDays, field.TypeInt, value) } + if value, ok := _u.mutation.ImagePrice1k(); ok { + _spec.SetField(group.FieldImagePrice1k, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedImagePrice1k(); ok { + _spec.AddField(group.FieldImagePrice1k, field.TypeFloat64, value) + } + if _u.mutation.ImagePrice1kCleared() { + _spec.ClearField(group.FieldImagePrice1k, field.TypeFloat64) + } + if value, ok := _u.mutation.ImagePrice2k(); ok { + _spec.SetField(group.FieldImagePrice2k, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedImagePrice2k(); ok { + _spec.AddField(group.FieldImagePrice2k, field.TypeFloat64, value) + } + if _u.mutation.ImagePrice2kCleared() { + _spec.ClearField(group.FieldImagePrice2k, field.TypeFloat64) + } + if value, ok := _u.mutation.ImagePrice4k(); ok { + _spec.SetField(group.FieldImagePrice4k, field.TypeFloat64, value) + } + if value, ok := _u.mutation.AddedImagePrice4k(); ok { + _spec.AddField(group.FieldImagePrice4k, field.TypeFloat64, value) + } + if _u.mutation.ImagePrice4kCleared() { + _spec.ClearField(group.FieldImagePrice4k, field.TypeFloat64) + } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index b85630ea..f5a4d1de 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -215,6 +215,9 @@ var ( {Name: "weekly_limit_usd", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "monthly_limit_usd", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, {Name: "default_validity_days", Type: field.TypeInt, Default: 30}, + {Name: "image_price_1k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, + {Name: "image_price_2k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, + {Name: "image_price_4k", Type: field.TypeFloat64, Nullable: true, SchemaType: map[string]string{"postgres": "decimal(20,8)"}}, } // GroupsTable holds the schema information for the "groups" table. GroupsTable = &schema.Table{ @@ -367,6 +370,8 @@ var ( {Name: "stream", Type: field.TypeBool, Default: false}, {Name: "duration_ms", Type: field.TypeInt, Nullable: true}, {Name: "first_token_ms", Type: field.TypeInt, Nullable: true}, + {Name: "image_count", Type: field.TypeInt, Default: 0}, + {Name: "image_size", Type: field.TypeString, Nullable: true, Size: 10}, {Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}}, {Name: "api_key_id", Type: field.TypeInt64}, {Name: "account_id", Type: field.TypeInt64}, @@ -382,31 +387,31 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "usage_logs_api_keys_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[21]}, + Columns: []*schema.Column{UsageLogsColumns[23]}, RefColumns: []*schema.Column{APIKeysColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "usage_logs_accounts_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[22]}, + Columns: []*schema.Column{UsageLogsColumns[24]}, RefColumns: []*schema.Column{AccountsColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "usage_logs_groups_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[23]}, + Columns: []*schema.Column{UsageLogsColumns[25]}, RefColumns: []*schema.Column{GroupsColumns[0]}, OnDelete: schema.SetNull, }, { Symbol: "usage_logs_users_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[24]}, + Columns: []*schema.Column{UsageLogsColumns[26]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "usage_logs_user_subscriptions_usage_logs", - Columns: []*schema.Column{UsageLogsColumns[25]}, + Columns: []*schema.Column{UsageLogsColumns[27]}, RefColumns: []*schema.Column{UserSubscriptionsColumns[0]}, OnDelete: schema.SetNull, }, @@ -415,32 +420,32 @@ var ( { Name: "usagelog_user_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[24]}, + Columns: []*schema.Column{UsageLogsColumns[26]}, }, { Name: "usagelog_api_key_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[21]}, + Columns: []*schema.Column{UsageLogsColumns[23]}, }, { Name: "usagelog_account_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[22]}, + Columns: []*schema.Column{UsageLogsColumns[24]}, }, { Name: "usagelog_group_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[23]}, + Columns: []*schema.Column{UsageLogsColumns[25]}, }, { Name: "usagelog_subscription_id", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[25]}, + Columns: []*schema.Column{UsageLogsColumns[27]}, }, { Name: "usagelog_created_at", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[20]}, + Columns: []*schema.Column{UsageLogsColumns[22]}, }, { Name: "usagelog_model", @@ -455,12 +460,12 @@ var ( { Name: "usagelog_user_id_created_at", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[24], UsageLogsColumns[20]}, + Columns: []*schema.Column{UsageLogsColumns[26], UsageLogsColumns[22]}, }, { Name: "usagelog_api_key_id_created_at", Unique: false, - Columns: []*schema.Column{UsageLogsColumns[21], UsageLogsColumns[20]}, + Columns: []*schema.Column{UsageLogsColumns[23], UsageLogsColumns[22]}, }, }, } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 6a64b16c..1fd32f1d 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -3384,6 +3384,12 @@ type GroupMutation struct { addmonthly_limit_usd *float64 default_validity_days *int adddefault_validity_days *int + image_price_1k *float64 + addimage_price_1k *float64 + image_price_2k *float64 + addimage_price_2k *float64 + image_price_4k *float64 + addimage_price_4k *float64 clearedFields map[string]struct{} api_keys map[int64]struct{} removedapi_keys map[int64]struct{} @@ -4178,6 +4184,216 @@ func (m *GroupMutation) ResetDefaultValidityDays() { m.adddefault_validity_days = nil } +// SetImagePrice1k sets the "image_price_1k" field. +func (m *GroupMutation) SetImagePrice1k(f float64) { + m.image_price_1k = &f + m.addimage_price_1k = nil +} + +// ImagePrice1k returns the value of the "image_price_1k" field in the mutation. +func (m *GroupMutation) ImagePrice1k() (r float64, exists bool) { + v := m.image_price_1k + if v == nil { + return + } + return *v, true +} + +// OldImagePrice1k returns the old "image_price_1k" 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) OldImagePrice1k(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldImagePrice1k is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldImagePrice1k requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldImagePrice1k: %w", err) + } + return oldValue.ImagePrice1k, nil +} + +// AddImagePrice1k adds f to the "image_price_1k" field. +func (m *GroupMutation) AddImagePrice1k(f float64) { + if m.addimage_price_1k != nil { + *m.addimage_price_1k += f + } else { + m.addimage_price_1k = &f + } +} + +// AddedImagePrice1k returns the value that was added to the "image_price_1k" field in this mutation. +func (m *GroupMutation) AddedImagePrice1k() (r float64, exists bool) { + v := m.addimage_price_1k + if v == nil { + return + } + return *v, true +} + +// ClearImagePrice1k clears the value of the "image_price_1k" field. +func (m *GroupMutation) ClearImagePrice1k() { + m.image_price_1k = nil + m.addimage_price_1k = nil + m.clearedFields[group.FieldImagePrice1k] = struct{}{} +} + +// ImagePrice1kCleared returns if the "image_price_1k" field was cleared in this mutation. +func (m *GroupMutation) ImagePrice1kCleared() bool { + _, ok := m.clearedFields[group.FieldImagePrice1k] + return ok +} + +// ResetImagePrice1k resets all changes to the "image_price_1k" field. +func (m *GroupMutation) ResetImagePrice1k() { + m.image_price_1k = nil + m.addimage_price_1k = nil + delete(m.clearedFields, group.FieldImagePrice1k) +} + +// SetImagePrice2k sets the "image_price_2k" field. +func (m *GroupMutation) SetImagePrice2k(f float64) { + m.image_price_2k = &f + m.addimage_price_2k = nil +} + +// ImagePrice2k returns the value of the "image_price_2k" field in the mutation. +func (m *GroupMutation) ImagePrice2k() (r float64, exists bool) { + v := m.image_price_2k + if v == nil { + return + } + return *v, true +} + +// OldImagePrice2k returns the old "image_price_2k" 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) OldImagePrice2k(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldImagePrice2k is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldImagePrice2k requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldImagePrice2k: %w", err) + } + return oldValue.ImagePrice2k, nil +} + +// AddImagePrice2k adds f to the "image_price_2k" field. +func (m *GroupMutation) AddImagePrice2k(f float64) { + if m.addimage_price_2k != nil { + *m.addimage_price_2k += f + } else { + m.addimage_price_2k = &f + } +} + +// AddedImagePrice2k returns the value that was added to the "image_price_2k" field in this mutation. +func (m *GroupMutation) AddedImagePrice2k() (r float64, exists bool) { + v := m.addimage_price_2k + if v == nil { + return + } + return *v, true +} + +// ClearImagePrice2k clears the value of the "image_price_2k" field. +func (m *GroupMutation) ClearImagePrice2k() { + m.image_price_2k = nil + m.addimage_price_2k = nil + m.clearedFields[group.FieldImagePrice2k] = struct{}{} +} + +// ImagePrice2kCleared returns if the "image_price_2k" field was cleared in this mutation. +func (m *GroupMutation) ImagePrice2kCleared() bool { + _, ok := m.clearedFields[group.FieldImagePrice2k] + return ok +} + +// ResetImagePrice2k resets all changes to the "image_price_2k" field. +func (m *GroupMutation) ResetImagePrice2k() { + m.image_price_2k = nil + m.addimage_price_2k = nil + delete(m.clearedFields, group.FieldImagePrice2k) +} + +// SetImagePrice4k sets the "image_price_4k" field. +func (m *GroupMutation) SetImagePrice4k(f float64) { + m.image_price_4k = &f + m.addimage_price_4k = nil +} + +// ImagePrice4k returns the value of the "image_price_4k" field in the mutation. +func (m *GroupMutation) ImagePrice4k() (r float64, exists bool) { + v := m.image_price_4k + if v == nil { + return + } + return *v, true +} + +// OldImagePrice4k returns the old "image_price_4k" 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) OldImagePrice4k(ctx context.Context) (v *float64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldImagePrice4k is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldImagePrice4k requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldImagePrice4k: %w", err) + } + return oldValue.ImagePrice4k, nil +} + +// AddImagePrice4k adds f to the "image_price_4k" field. +func (m *GroupMutation) AddImagePrice4k(f float64) { + if m.addimage_price_4k != nil { + *m.addimage_price_4k += f + } else { + m.addimage_price_4k = &f + } +} + +// AddedImagePrice4k returns the value that was added to the "image_price_4k" field in this mutation. +func (m *GroupMutation) AddedImagePrice4k() (r float64, exists bool) { + v := m.addimage_price_4k + if v == nil { + return + } + return *v, true +} + +// ClearImagePrice4k clears the value of the "image_price_4k" field. +func (m *GroupMutation) ClearImagePrice4k() { + m.image_price_4k = nil + m.addimage_price_4k = nil + m.clearedFields[group.FieldImagePrice4k] = struct{}{} +} + +// ImagePrice4kCleared returns if the "image_price_4k" field was cleared in this mutation. +func (m *GroupMutation) ImagePrice4kCleared() bool { + _, ok := m.clearedFields[group.FieldImagePrice4k] + return ok +} + +// ResetImagePrice4k resets all changes to the "image_price_4k" field. +func (m *GroupMutation) ResetImagePrice4k() { + m.image_price_4k = nil + m.addimage_price_4k = nil + delete(m.clearedFields, group.FieldImagePrice4k) +} + // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids. func (m *GroupMutation) AddAPIKeyIDs(ids ...int64) { if m.api_keys == nil { @@ -4536,7 +4752,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, 14) + fields := make([]string, 0, 17) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -4579,6 +4795,15 @@ func (m *GroupMutation) Fields() []string { if m.default_validity_days != nil { fields = append(fields, group.FieldDefaultValidityDays) } + if m.image_price_1k != nil { + fields = append(fields, group.FieldImagePrice1k) + } + if m.image_price_2k != nil { + fields = append(fields, group.FieldImagePrice2k) + } + if m.image_price_4k != nil { + fields = append(fields, group.FieldImagePrice4k) + } return fields } @@ -4615,6 +4840,12 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.MonthlyLimitUsd() case group.FieldDefaultValidityDays: return m.DefaultValidityDays() + case group.FieldImagePrice1k: + return m.ImagePrice1k() + case group.FieldImagePrice2k: + return m.ImagePrice2k() + case group.FieldImagePrice4k: + return m.ImagePrice4k() } return nil, false } @@ -4652,6 +4883,12 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldMonthlyLimitUsd(ctx) case group.FieldDefaultValidityDays: return m.OldDefaultValidityDays(ctx) + case group.FieldImagePrice1k: + return m.OldImagePrice1k(ctx) + case group.FieldImagePrice2k: + return m.OldImagePrice2k(ctx) + case group.FieldImagePrice4k: + return m.OldImagePrice4k(ctx) } return nil, fmt.Errorf("unknown Group field %s", name) } @@ -4759,6 +4996,27 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetDefaultValidityDays(v) return nil + case group.FieldImagePrice1k: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetImagePrice1k(v) + return nil + case group.FieldImagePrice2k: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetImagePrice2k(v) + return nil + case group.FieldImagePrice4k: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetImagePrice4k(v) + return nil } return fmt.Errorf("unknown Group field %s", name) } @@ -4782,6 +5040,15 @@ func (m *GroupMutation) AddedFields() []string { if m.adddefault_validity_days != nil { fields = append(fields, group.FieldDefaultValidityDays) } + if m.addimage_price_1k != nil { + fields = append(fields, group.FieldImagePrice1k) + } + if m.addimage_price_2k != nil { + fields = append(fields, group.FieldImagePrice2k) + } + if m.addimage_price_4k != nil { + fields = append(fields, group.FieldImagePrice4k) + } return fields } @@ -4800,6 +5067,12 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { return m.AddedMonthlyLimitUsd() case group.FieldDefaultValidityDays: return m.AddedDefaultValidityDays() + case group.FieldImagePrice1k: + return m.AddedImagePrice1k() + case group.FieldImagePrice2k: + return m.AddedImagePrice2k() + case group.FieldImagePrice4k: + return m.AddedImagePrice4k() } return nil, false } @@ -4844,6 +5117,27 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error { } m.AddDefaultValidityDays(v) return nil + case group.FieldImagePrice1k: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddImagePrice1k(v) + return nil + case group.FieldImagePrice2k: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddImagePrice2k(v) + return nil + case group.FieldImagePrice4k: + v, ok := value.(float64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddImagePrice4k(v) + return nil } return fmt.Errorf("unknown Group numeric field %s", name) } @@ -4867,6 +5161,15 @@ func (m *GroupMutation) ClearedFields() []string { if m.FieldCleared(group.FieldMonthlyLimitUsd) { fields = append(fields, group.FieldMonthlyLimitUsd) } + if m.FieldCleared(group.FieldImagePrice1k) { + fields = append(fields, group.FieldImagePrice1k) + } + if m.FieldCleared(group.FieldImagePrice2k) { + fields = append(fields, group.FieldImagePrice2k) + } + if m.FieldCleared(group.FieldImagePrice4k) { + fields = append(fields, group.FieldImagePrice4k) + } return fields } @@ -4896,6 +5199,15 @@ func (m *GroupMutation) ClearField(name string) error { case group.FieldMonthlyLimitUsd: m.ClearMonthlyLimitUsd() return nil + case group.FieldImagePrice1k: + m.ClearImagePrice1k() + return nil + case group.FieldImagePrice2k: + m.ClearImagePrice2k() + return nil + case group.FieldImagePrice4k: + m.ClearImagePrice4k() + return nil } return fmt.Errorf("unknown Group nullable field %s", name) } @@ -4946,6 +5258,15 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldDefaultValidityDays: m.ResetDefaultValidityDays() return nil + case group.FieldImagePrice1k: + m.ResetImagePrice1k() + return nil + case group.FieldImagePrice2k: + m.ResetImagePrice2k() + return nil + case group.FieldImagePrice4k: + m.ResetImagePrice4k() + return nil } return fmt.Errorf("unknown Group field %s", name) } @@ -7713,6 +8034,9 @@ type UsageLogMutation struct { addduration_ms *int first_token_ms *int addfirst_token_ms *int + image_count *int + addimage_count *int + image_size *string created_at *time.Time clearedFields map[string]struct{} user *int64 @@ -9066,6 +9390,111 @@ func (m *UsageLogMutation) ResetFirstTokenMs() { delete(m.clearedFields, usagelog.FieldFirstTokenMs) } +// SetImageCount sets the "image_count" field. +func (m *UsageLogMutation) SetImageCount(i int) { + m.image_count = &i + m.addimage_count = nil +} + +// ImageCount returns the value of the "image_count" field in the mutation. +func (m *UsageLogMutation) ImageCount() (r int, exists bool) { + v := m.image_count + if v == nil { + return + } + return *v, true +} + +// OldImageCount returns the old "image_count" field's value of the UsageLog entity. +// If the UsageLog 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 *UsageLogMutation) OldImageCount(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldImageCount is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldImageCount requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldImageCount: %w", err) + } + return oldValue.ImageCount, nil +} + +// AddImageCount adds i to the "image_count" field. +func (m *UsageLogMutation) AddImageCount(i int) { + if m.addimage_count != nil { + *m.addimage_count += i + } else { + m.addimage_count = &i + } +} + +// AddedImageCount returns the value that was added to the "image_count" field in this mutation. +func (m *UsageLogMutation) AddedImageCount() (r int, exists bool) { + v := m.addimage_count + if v == nil { + return + } + return *v, true +} + +// ResetImageCount resets all changes to the "image_count" field. +func (m *UsageLogMutation) ResetImageCount() { + m.image_count = nil + m.addimage_count = nil +} + +// SetImageSize sets the "image_size" field. +func (m *UsageLogMutation) SetImageSize(s string) { + m.image_size = &s +} + +// ImageSize returns the value of the "image_size" field in the mutation. +func (m *UsageLogMutation) ImageSize() (r string, exists bool) { + v := m.image_size + if v == nil { + return + } + return *v, true +} + +// OldImageSize returns the old "image_size" field's value of the UsageLog entity. +// If the UsageLog 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 *UsageLogMutation) OldImageSize(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldImageSize is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldImageSize requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldImageSize: %w", err) + } + return oldValue.ImageSize, nil +} + +// ClearImageSize clears the value of the "image_size" field. +func (m *UsageLogMutation) ClearImageSize() { + m.image_size = nil + m.clearedFields[usagelog.FieldImageSize] = struct{}{} +} + +// ImageSizeCleared returns if the "image_size" field was cleared in this mutation. +func (m *UsageLogMutation) ImageSizeCleared() bool { + _, ok := m.clearedFields[usagelog.FieldImageSize] + return ok +} + +// ResetImageSize resets all changes to the "image_size" field. +func (m *UsageLogMutation) ResetImageSize() { + m.image_size = nil + delete(m.clearedFields, usagelog.FieldImageSize) +} + // SetCreatedAt sets the "created_at" field. func (m *UsageLogMutation) SetCreatedAt(t time.Time) { m.created_at = &t @@ -9271,7 +9700,7 @@ func (m *UsageLogMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *UsageLogMutation) Fields() []string { - fields := make([]string, 0, 25) + fields := make([]string, 0, 27) if m.user != nil { fields = append(fields, usagelog.FieldUserID) } @@ -9344,6 +9773,12 @@ func (m *UsageLogMutation) Fields() []string { if m.first_token_ms != nil { fields = append(fields, usagelog.FieldFirstTokenMs) } + if m.image_count != nil { + fields = append(fields, usagelog.FieldImageCount) + } + if m.image_size != nil { + fields = append(fields, usagelog.FieldImageSize) + } if m.created_at != nil { fields = append(fields, usagelog.FieldCreatedAt) } @@ -9403,6 +9838,10 @@ func (m *UsageLogMutation) Field(name string) (ent.Value, bool) { return m.DurationMs() case usagelog.FieldFirstTokenMs: return m.FirstTokenMs() + case usagelog.FieldImageCount: + return m.ImageCount() + case usagelog.FieldImageSize: + return m.ImageSize() case usagelog.FieldCreatedAt: return m.CreatedAt() } @@ -9462,6 +9901,10 @@ func (m *UsageLogMutation) OldField(ctx context.Context, name string) (ent.Value return m.OldDurationMs(ctx) case usagelog.FieldFirstTokenMs: return m.OldFirstTokenMs(ctx) + case usagelog.FieldImageCount: + return m.OldImageCount(ctx) + case usagelog.FieldImageSize: + return m.OldImageSize(ctx) case usagelog.FieldCreatedAt: return m.OldCreatedAt(ctx) } @@ -9641,6 +10084,20 @@ func (m *UsageLogMutation) SetField(name string, value ent.Value) error { } m.SetFirstTokenMs(v) return nil + case usagelog.FieldImageCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetImageCount(v) + return nil + case usagelog.FieldImageSize: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetImageSize(v) + return nil case usagelog.FieldCreatedAt: v, ok := value.(time.Time) if !ok { @@ -9704,6 +10161,9 @@ func (m *UsageLogMutation) AddedFields() []string { if m.addfirst_token_ms != nil { fields = append(fields, usagelog.FieldFirstTokenMs) } + if m.addimage_count != nil { + fields = append(fields, usagelog.FieldImageCount) + } return fields } @@ -9744,6 +10204,8 @@ func (m *UsageLogMutation) AddedField(name string) (ent.Value, bool) { return m.AddedDurationMs() case usagelog.FieldFirstTokenMs: return m.AddedFirstTokenMs() + case usagelog.FieldImageCount: + return m.AddedImageCount() } return nil, false } @@ -9865,6 +10327,13 @@ func (m *UsageLogMutation) AddField(name string, value ent.Value) error { } m.AddFirstTokenMs(v) return nil + case usagelog.FieldImageCount: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddImageCount(v) + return nil } return fmt.Errorf("unknown UsageLog numeric field %s", name) } @@ -9885,6 +10354,9 @@ func (m *UsageLogMutation) ClearedFields() []string { if m.FieldCleared(usagelog.FieldFirstTokenMs) { fields = append(fields, usagelog.FieldFirstTokenMs) } + if m.FieldCleared(usagelog.FieldImageSize) { + fields = append(fields, usagelog.FieldImageSize) + } return fields } @@ -9911,6 +10383,9 @@ func (m *UsageLogMutation) ClearField(name string) error { case usagelog.FieldFirstTokenMs: m.ClearFirstTokenMs() return nil + case usagelog.FieldImageSize: + m.ClearImageSize() + return nil } return fmt.Errorf("unknown UsageLog nullable field %s", name) } @@ -9991,6 +10466,12 @@ func (m *UsageLogMutation) ResetField(name string) error { case usagelog.FieldFirstTokenMs: m.ResetFirstTokenMs() return nil + case usagelog.FieldImageCount: + m.ResetImageCount() + return nil + case usagelog.FieldImageSize: + m.ResetImageSize() + return nil case usagelog.FieldCreatedAt: m.ResetCreatedAt() return nil diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 517e7195..d57b690d 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -521,8 +521,16 @@ func init() { usagelogDescStream := usagelogFields[21].Descriptor() // usagelog.DefaultStream holds the default value on creation for the stream field. usagelog.DefaultStream = usagelogDescStream.Default.(bool) + // usagelogDescImageCount is the schema descriptor for image_count field. + usagelogDescImageCount := usagelogFields[24].Descriptor() + // usagelog.DefaultImageCount holds the default value on creation for the image_count field. + usagelog.DefaultImageCount = usagelogDescImageCount.Default.(int) + // usagelogDescImageSize is the schema descriptor for image_size field. + usagelogDescImageSize := usagelogFields[25].Descriptor() + // usagelog.ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save. + usagelog.ImageSizeValidator = usagelogDescImageSize.Validators[0].(func(string) error) // usagelogDescCreatedAt is the schema descriptor for created_at field. - usagelogDescCreatedAt := usagelogFields[24].Descriptor() + usagelogDescCreatedAt := usagelogFields[26].Descriptor() // usagelog.DefaultCreatedAt holds the default value on creation for the created_at field. usagelog.DefaultCreatedAt = usagelogDescCreatedAt.Default.(func() time.Time) userMixin := schema.User{}.Mixin() diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index 93dab1ab..7b5f77b1 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -72,6 +72,20 @@ func (Group) Fields() []ent.Field { SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), field.Int("default_validity_days"). Default(30), + + // 图片生成计费配置(antigravity 和 gemini 平台使用) + field.Float("image_price_1k"). + Optional(). + Nillable(). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), + field.Float("image_price_2k"). + Optional(). + Nillable(). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), + field.Float("image_price_4k"). + Optional(). + Nillable(). + SchemaType(map[string]string{dialect.Postgres: "decimal(20,8)"}), } } diff --git a/backend/ent/schema/usage_log.go b/backend/ent/schema/usage_log.go index 81effa46..af99904d 100644 --- a/backend/ent/schema/usage_log.go +++ b/backend/ent/schema/usage_log.go @@ -97,6 +97,14 @@ func (UsageLog) Fields() []ent.Field { Optional(). Nillable(), + // 图片生成字段(仅 gemini-3-pro-image 等图片模型使用) + field.Int("image_count"). + Default(0), + field.String("image_size"). + MaxLen(10). + Optional(). + Nillable(), + // 时间戳(只有 created_at,日志不可修改) field.Time("created_at"). Default(time.Now). diff --git a/backend/ent/usagelog.go b/backend/ent/usagelog.go index 75e3173d..35cd337f 100644 --- a/backend/ent/usagelog.go +++ b/backend/ent/usagelog.go @@ -70,6 +70,10 @@ type UsageLog struct { DurationMs *int `json:"duration_ms,omitempty"` // FirstTokenMs holds the value of the "first_token_ms" field. FirstTokenMs *int `json:"first_token_ms,omitempty"` + // ImageCount holds the value of the "image_count" field. + ImageCount int `json:"image_count,omitempty"` + // ImageSize holds the value of the "image_size" field. + ImageSize *string `json:"image_size,omitempty"` // CreatedAt holds the value of the "created_at" field. CreatedAt time.Time `json:"created_at,omitempty"` // Edges holds the relations/edges for other nodes in the graph. @@ -159,9 +163,9 @@ func (*UsageLog) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case usagelog.FieldInputCost, usagelog.FieldOutputCost, usagelog.FieldCacheCreationCost, usagelog.FieldCacheReadCost, usagelog.FieldTotalCost, usagelog.FieldActualCost, usagelog.FieldRateMultiplier: values[i] = new(sql.NullFloat64) - case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs: + case usagelog.FieldID, usagelog.FieldUserID, usagelog.FieldAPIKeyID, usagelog.FieldAccountID, usagelog.FieldGroupID, usagelog.FieldSubscriptionID, usagelog.FieldInputTokens, usagelog.FieldOutputTokens, usagelog.FieldCacheCreationTokens, usagelog.FieldCacheReadTokens, usagelog.FieldCacheCreation5mTokens, usagelog.FieldCacheCreation1hTokens, usagelog.FieldBillingType, usagelog.FieldDurationMs, usagelog.FieldFirstTokenMs, usagelog.FieldImageCount: values[i] = new(sql.NullInt64) - case usagelog.FieldRequestID, usagelog.FieldModel: + case usagelog.FieldRequestID, usagelog.FieldModel, usagelog.FieldImageSize: values[i] = new(sql.NullString) case usagelog.FieldCreatedAt: values[i] = new(sql.NullTime) @@ -334,6 +338,19 @@ func (_m *UsageLog) assignValues(columns []string, values []any) error { _m.FirstTokenMs = new(int) *_m.FirstTokenMs = int(value.Int64) } + case usagelog.FieldImageCount: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field image_count", values[i]) + } else if value.Valid { + _m.ImageCount = int(value.Int64) + } + case usagelog.FieldImageSize: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field image_size", values[i]) + } else if value.Valid { + _m.ImageSize = new(string) + *_m.ImageSize = value.String + } case usagelog.FieldCreatedAt: if value, ok := values[i].(*sql.NullTime); !ok { return fmt.Errorf("unexpected type %T for field created_at", values[i]) @@ -481,6 +498,14 @@ func (_m *UsageLog) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") + builder.WriteString("image_count=") + builder.WriteString(fmt.Sprintf("%v", _m.ImageCount)) + builder.WriteString(", ") + if v := _m.ImageSize; v != nil { + builder.WriteString("image_size=") + builder.WriteString(*v) + } + builder.WriteString(", ") builder.WriteString("created_at=") builder.WriteString(_m.CreatedAt.Format(time.ANSIC)) builder.WriteByte(')') diff --git a/backend/ent/usagelog/usagelog.go b/backend/ent/usagelog/usagelog.go index 139721c4..bc0cedc8 100644 --- a/backend/ent/usagelog/usagelog.go +++ b/backend/ent/usagelog/usagelog.go @@ -62,6 +62,10 @@ const ( FieldDurationMs = "duration_ms" // FieldFirstTokenMs holds the string denoting the first_token_ms field in the database. FieldFirstTokenMs = "first_token_ms" + // FieldImageCount holds the string denoting the image_count field in the database. + FieldImageCount = "image_count" + // FieldImageSize holds the string denoting the image_size field in the database. + FieldImageSize = "image_size" // FieldCreatedAt holds the string denoting the created_at field in the database. FieldCreatedAt = "created_at" // EdgeUser holds the string denoting the user edge name in mutations. @@ -140,6 +144,8 @@ var Columns = []string{ FieldStream, FieldDurationMs, FieldFirstTokenMs, + FieldImageCount, + FieldImageSize, FieldCreatedAt, } @@ -188,6 +194,10 @@ var ( DefaultBillingType int8 // DefaultStream holds the default value on creation for the "stream" field. DefaultStream bool + // DefaultImageCount holds the default value on creation for the "image_count" field. + DefaultImageCount int + // ImageSizeValidator is a validator for the "image_size" field. It is called by the builders before save. + ImageSizeValidator func(string) error // DefaultCreatedAt holds the default value on creation for the "created_at" field. DefaultCreatedAt func() time.Time ) @@ -320,6 +330,16 @@ func ByFirstTokenMs(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldFirstTokenMs, opts...).ToFunc() } +// ByImageCount orders the results by the image_count field. +func ByImageCount(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldImageCount, opts...).ToFunc() +} + +// ByImageSize orders the results by the image_size field. +func ByImageSize(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldImageSize, opts...).ToFunc() +} + // ByCreatedAt orders the results by the created_at field. func ByCreatedAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldCreatedAt, opts...).ToFunc() diff --git a/backend/ent/usagelog/where.go b/backend/ent/usagelog/where.go index 9db01140..7d9edae1 100644 --- a/backend/ent/usagelog/where.go +++ b/backend/ent/usagelog/where.go @@ -175,6 +175,16 @@ func FirstTokenMs(v int) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldFirstTokenMs, v)) } +// ImageCount applies equality check predicate on the "image_count" field. It's identical to ImageCountEQ. +func ImageCount(v int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v)) +} + +// ImageSize applies equality check predicate on the "image_size" field. It's identical to ImageSizeEQ. +func ImageSize(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v)) +} + // CreatedAt applies equality check predicate on the "created_at" field. It's identical to CreatedAtEQ. func CreatedAt(v time.Time) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v)) @@ -1100,6 +1110,121 @@ func FirstTokenMsNotNil() predicate.UsageLog { return predicate.UsageLog(sql.FieldNotNull(FieldFirstTokenMs)) } +// ImageCountEQ applies the EQ predicate on the "image_count" field. +func ImageCountEQ(v int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEQ(FieldImageCount, v)) +} + +// ImageCountNEQ applies the NEQ predicate on the "image_count" field. +func ImageCountNEQ(v int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldNEQ(FieldImageCount, v)) +} + +// ImageCountIn applies the In predicate on the "image_count" field. +func ImageCountIn(vs ...int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldIn(FieldImageCount, vs...)) +} + +// ImageCountNotIn applies the NotIn predicate on the "image_count" field. +func ImageCountNotIn(vs ...int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldNotIn(FieldImageCount, vs...)) +} + +// ImageCountGT applies the GT predicate on the "image_count" field. +func ImageCountGT(v int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldGT(FieldImageCount, v)) +} + +// ImageCountGTE applies the GTE predicate on the "image_count" field. +func ImageCountGTE(v int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldGTE(FieldImageCount, v)) +} + +// ImageCountLT applies the LT predicate on the "image_count" field. +func ImageCountLT(v int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldLT(FieldImageCount, v)) +} + +// ImageCountLTE applies the LTE predicate on the "image_count" field. +func ImageCountLTE(v int) predicate.UsageLog { + return predicate.UsageLog(sql.FieldLTE(FieldImageCount, v)) +} + +// ImageSizeEQ applies the EQ predicate on the "image_size" field. +func ImageSizeEQ(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEQ(FieldImageSize, v)) +} + +// ImageSizeNEQ applies the NEQ predicate on the "image_size" field. +func ImageSizeNEQ(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldNEQ(FieldImageSize, v)) +} + +// ImageSizeIn applies the In predicate on the "image_size" field. +func ImageSizeIn(vs ...string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldIn(FieldImageSize, vs...)) +} + +// ImageSizeNotIn applies the NotIn predicate on the "image_size" field. +func ImageSizeNotIn(vs ...string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldNotIn(FieldImageSize, vs...)) +} + +// ImageSizeGT applies the GT predicate on the "image_size" field. +func ImageSizeGT(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldGT(FieldImageSize, v)) +} + +// ImageSizeGTE applies the GTE predicate on the "image_size" field. +func ImageSizeGTE(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldGTE(FieldImageSize, v)) +} + +// ImageSizeLT applies the LT predicate on the "image_size" field. +func ImageSizeLT(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldLT(FieldImageSize, v)) +} + +// ImageSizeLTE applies the LTE predicate on the "image_size" field. +func ImageSizeLTE(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldLTE(FieldImageSize, v)) +} + +// ImageSizeContains applies the Contains predicate on the "image_size" field. +func ImageSizeContains(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldContains(FieldImageSize, v)) +} + +// ImageSizeHasPrefix applies the HasPrefix predicate on the "image_size" field. +func ImageSizeHasPrefix(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldHasPrefix(FieldImageSize, v)) +} + +// ImageSizeHasSuffix applies the HasSuffix predicate on the "image_size" field. +func ImageSizeHasSuffix(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldHasSuffix(FieldImageSize, v)) +} + +// ImageSizeIsNil applies the IsNil predicate on the "image_size" field. +func ImageSizeIsNil() predicate.UsageLog { + return predicate.UsageLog(sql.FieldIsNull(FieldImageSize)) +} + +// ImageSizeNotNil applies the NotNil predicate on the "image_size" field. +func ImageSizeNotNil() predicate.UsageLog { + return predicate.UsageLog(sql.FieldNotNull(FieldImageSize)) +} + +// ImageSizeEqualFold applies the EqualFold predicate on the "image_size" field. +func ImageSizeEqualFold(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldEqualFold(FieldImageSize, v)) +} + +// ImageSizeContainsFold applies the ContainsFold predicate on the "image_size" field. +func ImageSizeContainsFold(v string) predicate.UsageLog { + return predicate.UsageLog(sql.FieldContainsFold(FieldImageSize, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.UsageLog { return predicate.UsageLog(sql.FieldEQ(FieldCreatedAt, v)) diff --git a/backend/ent/usagelog_create.go b/backend/ent/usagelog_create.go index 36f3d277..ef4a9ca2 100644 --- a/backend/ent/usagelog_create.go +++ b/backend/ent/usagelog_create.go @@ -323,6 +323,34 @@ func (_c *UsageLogCreate) SetNillableFirstTokenMs(v *int) *UsageLogCreate { return _c } +// SetImageCount sets the "image_count" field. +func (_c *UsageLogCreate) SetImageCount(v int) *UsageLogCreate { + _c.mutation.SetImageCount(v) + return _c +} + +// SetNillableImageCount sets the "image_count" field if the given value is not nil. +func (_c *UsageLogCreate) SetNillableImageCount(v *int) *UsageLogCreate { + if v != nil { + _c.SetImageCount(*v) + } + return _c +} + +// SetImageSize sets the "image_size" field. +func (_c *UsageLogCreate) SetImageSize(v string) *UsageLogCreate { + _c.mutation.SetImageSize(v) + return _c +} + +// SetNillableImageSize sets the "image_size" field if the given value is not nil. +func (_c *UsageLogCreate) SetNillableImageSize(v *string) *UsageLogCreate { + if v != nil { + _c.SetImageSize(*v) + } + return _c +} + // SetCreatedAt sets the "created_at" field. func (_c *UsageLogCreate) SetCreatedAt(v time.Time) *UsageLogCreate { _c.mutation.SetCreatedAt(v) @@ -457,6 +485,10 @@ func (_c *UsageLogCreate) defaults() { v := usagelog.DefaultStream _c.mutation.SetStream(v) } + if _, ok := _c.mutation.ImageCount(); !ok { + v := usagelog.DefaultImageCount + _c.mutation.SetImageCount(v) + } if _, ok := _c.mutation.CreatedAt(); !ok { v := usagelog.DefaultCreatedAt() _c.mutation.SetCreatedAt(v) @@ -535,6 +567,14 @@ func (_c *UsageLogCreate) check() error { if _, ok := _c.mutation.Stream(); !ok { return &ValidationError{Name: "stream", err: errors.New(`ent: missing required field "UsageLog.stream"`)} } + if _, ok := _c.mutation.ImageCount(); !ok { + return &ValidationError{Name: "image_count", err: errors.New(`ent: missing required field "UsageLog.image_count"`)} + } + if v, ok := _c.mutation.ImageSize(); ok { + if err := usagelog.ImageSizeValidator(v); err != nil { + return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)} + } + } if _, ok := _c.mutation.CreatedAt(); !ok { return &ValidationError{Name: "created_at", err: errors.New(`ent: missing required field "UsageLog.created_at"`)} } @@ -650,6 +690,14 @@ func (_c *UsageLogCreate) createSpec() (*UsageLog, *sqlgraph.CreateSpec) { _spec.SetField(usagelog.FieldFirstTokenMs, field.TypeInt, value) _node.FirstTokenMs = &value } + if value, ok := _c.mutation.ImageCount(); ok { + _spec.SetField(usagelog.FieldImageCount, field.TypeInt, value) + _node.ImageCount = value + } + if value, ok := _c.mutation.ImageSize(); ok { + _spec.SetField(usagelog.FieldImageSize, field.TypeString, value) + _node.ImageSize = &value + } if value, ok := _c.mutation.CreatedAt(); ok { _spec.SetField(usagelog.FieldCreatedAt, field.TypeTime, value) _node.CreatedAt = value @@ -1199,6 +1247,42 @@ func (u *UsageLogUpsert) ClearFirstTokenMs() *UsageLogUpsert { return u } +// SetImageCount sets the "image_count" field. +func (u *UsageLogUpsert) SetImageCount(v int) *UsageLogUpsert { + u.Set(usagelog.FieldImageCount, v) + return u +} + +// UpdateImageCount sets the "image_count" field to the value that was provided on create. +func (u *UsageLogUpsert) UpdateImageCount() *UsageLogUpsert { + u.SetExcluded(usagelog.FieldImageCount) + return u +} + +// AddImageCount adds v to the "image_count" field. +func (u *UsageLogUpsert) AddImageCount(v int) *UsageLogUpsert { + u.Add(usagelog.FieldImageCount, v) + return u +} + +// SetImageSize sets the "image_size" field. +func (u *UsageLogUpsert) SetImageSize(v string) *UsageLogUpsert { + u.Set(usagelog.FieldImageSize, v) + return u +} + +// UpdateImageSize sets the "image_size" field to the value that was provided on create. +func (u *UsageLogUpsert) UpdateImageSize() *UsageLogUpsert { + u.SetExcluded(usagelog.FieldImageSize) + return u +} + +// ClearImageSize clears the value of the "image_size" field. +func (u *UsageLogUpsert) ClearImageSize() *UsageLogUpsert { + u.SetNull(usagelog.FieldImageSize) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -1720,6 +1804,48 @@ func (u *UsageLogUpsertOne) ClearFirstTokenMs() *UsageLogUpsertOne { }) } +// SetImageCount sets the "image_count" field. +func (u *UsageLogUpsertOne) SetImageCount(v int) *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.SetImageCount(v) + }) +} + +// AddImageCount adds v to the "image_count" field. +func (u *UsageLogUpsertOne) AddImageCount(v int) *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.AddImageCount(v) + }) +} + +// UpdateImageCount sets the "image_count" field to the value that was provided on create. +func (u *UsageLogUpsertOne) UpdateImageCount() *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.UpdateImageCount() + }) +} + +// SetImageSize sets the "image_size" field. +func (u *UsageLogUpsertOne) SetImageSize(v string) *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.SetImageSize(v) + }) +} + +// UpdateImageSize sets the "image_size" field to the value that was provided on create. +func (u *UsageLogUpsertOne) UpdateImageSize() *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.UpdateImageSize() + }) +} + +// ClearImageSize clears the value of the "image_size" field. +func (u *UsageLogUpsertOne) ClearImageSize() *UsageLogUpsertOne { + return u.Update(func(s *UsageLogUpsert) { + s.ClearImageSize() + }) +} + // Exec executes the query. func (u *UsageLogUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -2407,6 +2533,48 @@ func (u *UsageLogUpsertBulk) ClearFirstTokenMs() *UsageLogUpsertBulk { }) } +// SetImageCount sets the "image_count" field. +func (u *UsageLogUpsertBulk) SetImageCount(v int) *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.SetImageCount(v) + }) +} + +// AddImageCount adds v to the "image_count" field. +func (u *UsageLogUpsertBulk) AddImageCount(v int) *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.AddImageCount(v) + }) +} + +// UpdateImageCount sets the "image_count" field to the value that was provided on create. +func (u *UsageLogUpsertBulk) UpdateImageCount() *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.UpdateImageCount() + }) +} + +// SetImageSize sets the "image_size" field. +func (u *UsageLogUpsertBulk) SetImageSize(v string) *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.SetImageSize(v) + }) +} + +// UpdateImageSize sets the "image_size" field to the value that was provided on create. +func (u *UsageLogUpsertBulk) UpdateImageSize() *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.UpdateImageSize() + }) +} + +// ClearImageSize clears the value of the "image_size" field. +func (u *UsageLogUpsertBulk) ClearImageSize() *UsageLogUpsertBulk { + return u.Update(func(s *UsageLogUpsert) { + s.ClearImageSize() + }) +} + // Exec executes the query. func (u *UsageLogUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/backend/ent/usagelog_update.go b/backend/ent/usagelog_update.go index 45ad2e2a..7eb2132b 100644 --- a/backend/ent/usagelog_update.go +++ b/backend/ent/usagelog_update.go @@ -504,6 +504,47 @@ func (_u *UsageLogUpdate) ClearFirstTokenMs() *UsageLogUpdate { return _u } +// SetImageCount sets the "image_count" field. +func (_u *UsageLogUpdate) SetImageCount(v int) *UsageLogUpdate { + _u.mutation.ResetImageCount() + _u.mutation.SetImageCount(v) + return _u +} + +// SetNillableImageCount sets the "image_count" field if the given value is not nil. +func (_u *UsageLogUpdate) SetNillableImageCount(v *int) *UsageLogUpdate { + if v != nil { + _u.SetImageCount(*v) + } + return _u +} + +// AddImageCount adds value to the "image_count" field. +func (_u *UsageLogUpdate) AddImageCount(v int) *UsageLogUpdate { + _u.mutation.AddImageCount(v) + return _u +} + +// SetImageSize sets the "image_size" field. +func (_u *UsageLogUpdate) SetImageSize(v string) *UsageLogUpdate { + _u.mutation.SetImageSize(v) + return _u +} + +// SetNillableImageSize sets the "image_size" field if the given value is not nil. +func (_u *UsageLogUpdate) SetNillableImageSize(v *string) *UsageLogUpdate { + if v != nil { + _u.SetImageSize(*v) + } + return _u +} + +// ClearImageSize clears the value of the "image_size" field. +func (_u *UsageLogUpdate) ClearImageSize() *UsageLogUpdate { + _u.mutation.ClearImageSize() + return _u +} + // SetUser sets the "user" edge to the User entity. func (_u *UsageLogUpdate) SetUser(v *User) *UsageLogUpdate { return _u.SetUserID(v.ID) @@ -603,6 +644,11 @@ func (_u *UsageLogUpdate) check() error { return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)} } } + if v, ok := _u.mutation.ImageSize(); ok { + if err := usagelog.ImageSizeValidator(v); err != nil { + return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)} + } + } if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "UsageLog.user"`) } @@ -738,6 +784,18 @@ func (_u *UsageLogUpdate) sqlSave(ctx context.Context) (_node int, err error) { if _u.mutation.FirstTokenMsCleared() { _spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt) } + if value, ok := _u.mutation.ImageCount(); ok { + _spec.SetField(usagelog.FieldImageCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedImageCount(); ok { + _spec.AddField(usagelog.FieldImageCount, field.TypeInt, value) + } + if value, ok := _u.mutation.ImageSize(); ok { + _spec.SetField(usagelog.FieldImageSize, field.TypeString, value) + } + if _u.mutation.ImageSizeCleared() { + _spec.ClearField(usagelog.FieldImageSize, field.TypeString) + } if _u.mutation.UserCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -1375,6 +1433,47 @@ func (_u *UsageLogUpdateOne) ClearFirstTokenMs() *UsageLogUpdateOne { return _u } +// SetImageCount sets the "image_count" field. +func (_u *UsageLogUpdateOne) SetImageCount(v int) *UsageLogUpdateOne { + _u.mutation.ResetImageCount() + _u.mutation.SetImageCount(v) + return _u +} + +// SetNillableImageCount sets the "image_count" field if the given value is not nil. +func (_u *UsageLogUpdateOne) SetNillableImageCount(v *int) *UsageLogUpdateOne { + if v != nil { + _u.SetImageCount(*v) + } + return _u +} + +// AddImageCount adds value to the "image_count" field. +func (_u *UsageLogUpdateOne) AddImageCount(v int) *UsageLogUpdateOne { + _u.mutation.AddImageCount(v) + return _u +} + +// SetImageSize sets the "image_size" field. +func (_u *UsageLogUpdateOne) SetImageSize(v string) *UsageLogUpdateOne { + _u.mutation.SetImageSize(v) + return _u +} + +// SetNillableImageSize sets the "image_size" field if the given value is not nil. +func (_u *UsageLogUpdateOne) SetNillableImageSize(v *string) *UsageLogUpdateOne { + if v != nil { + _u.SetImageSize(*v) + } + return _u +} + +// ClearImageSize clears the value of the "image_size" field. +func (_u *UsageLogUpdateOne) ClearImageSize() *UsageLogUpdateOne { + _u.mutation.ClearImageSize() + return _u +} + // SetUser sets the "user" edge to the User entity. func (_u *UsageLogUpdateOne) SetUser(v *User) *UsageLogUpdateOne { return _u.SetUserID(v.ID) @@ -1487,6 +1586,11 @@ func (_u *UsageLogUpdateOne) check() error { return &ValidationError{Name: "model", err: fmt.Errorf(`ent: validator failed for field "UsageLog.model": %w`, err)} } } + if v, ok := _u.mutation.ImageSize(); ok { + if err := usagelog.ImageSizeValidator(v); err != nil { + return &ValidationError{Name: "image_size", err: fmt.Errorf(`ent: validator failed for field "UsageLog.image_size": %w`, err)} + } + } if _u.mutation.UserCleared() && len(_u.mutation.UserIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "UsageLog.user"`) } @@ -1639,6 +1743,18 @@ func (_u *UsageLogUpdateOne) sqlSave(ctx context.Context) (_node *UsageLog, err if _u.mutation.FirstTokenMsCleared() { _spec.ClearField(usagelog.FieldFirstTokenMs, field.TypeInt) } + if value, ok := _u.mutation.ImageCount(); ok { + _spec.SetField(usagelog.FieldImageCount, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedImageCount(); ok { + _spec.AddField(usagelog.FieldImageCount, field.TypeInt, value) + } + if value, ok := _u.mutation.ImageSize(); ok { + _spec.SetField(usagelog.FieldImageSize, field.TypeString, value) + } + if _u.mutation.ImageSizeCleared() { + _spec.ClearField(usagelog.FieldImageSize, field.TypeString) + } if _u.mutation.UserCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, diff --git a/backend/internal/handler/admin/group_handler.go b/backend/internal/handler/admin/group_handler.go index 1ca54aaf..1d318271 100644 --- a/backend/internal/handler/admin/group_handler.go +++ b/backend/internal/handler/admin/group_handler.go @@ -33,6 +33,10 @@ type CreateGroupRequest struct { DailyLimitUSD *float64 `json:"daily_limit_usd"` WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` + // 图片生成计费配置(仅 antigravity 平台使用) + ImagePrice1K *float64 `json:"image_price_1k"` + ImagePrice2K *float64 `json:"image_price_2k"` + ImagePrice4K *float64 `json:"image_price_4k"` } // UpdateGroupRequest represents update group request @@ -47,6 +51,10 @@ type UpdateGroupRequest struct { DailyLimitUSD *float64 `json:"daily_limit_usd"` WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` + // 图片生成计费配置(仅 antigravity 平台使用) + ImagePrice1K *float64 `json:"image_price_1k"` + ImagePrice2K *float64 `json:"image_price_2k"` + ImagePrice4K *float64 `json:"image_price_4k"` } // List handles listing all groups with pagination @@ -139,6 +147,9 @@ func (h *GroupHandler) Create(c *gin.Context) { DailyLimitUSD: req.DailyLimitUSD, WeeklyLimitUSD: req.WeeklyLimitUSD, MonthlyLimitUSD: req.MonthlyLimitUSD, + ImagePrice1K: req.ImagePrice1K, + ImagePrice2K: req.ImagePrice2K, + ImagePrice4K: req.ImagePrice4K, }) if err != nil { response.ErrorFrom(c, err) @@ -174,6 +185,9 @@ func (h *GroupHandler) Update(c *gin.Context) { DailyLimitUSD: req.DailyLimitUSD, WeeklyLimitUSD: req.WeeklyLimitUSD, MonthlyLimitUSD: req.MonthlyLimitUSD, + ImagePrice1K: req.ImagePrice1K, + ImagePrice2K: req.ImagePrice2K, + ImagePrice4K: req.ImagePrice4K, }) if err != nil { response.ErrorFrom(c, err) diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index e449e752..5ecc7d84 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -78,6 +78,9 @@ func GroupFromServiceShallow(g *service.Group) *Group { DailyLimitUSD: g.DailyLimitUSD, WeeklyLimitUSD: g.WeeklyLimitUSD, MonthlyLimitUSD: g.MonthlyLimitUSD, + ImagePrice1K: g.ImagePrice1K, + ImagePrice2K: g.ImagePrice2K, + ImagePrice4K: g.ImagePrice4K, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, AccountCount: g.AccountCount, @@ -246,6 +249,8 @@ func UsageLogFromService(l *service.UsageLog) *UsageLog { Stream: l.Stream, DurationMs: l.DurationMs, FirstTokenMs: l.FirstTokenMs, + ImageCount: l.ImageCount, + ImageSize: l.ImageSize, CreatedAt: l.CreatedAt, User: UserFromServiceShallow(l.User), APIKey: APIKeyFromService(l.APIKey), diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 185056c9..c778f84e 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -47,6 +47,11 @@ type Group struct { WeeklyLimitUSD *float64 `json:"weekly_limit_usd"` MonthlyLimitUSD *float64 `json:"monthly_limit_usd"` + // 图片生成计费配置(仅 antigravity 平台使用) + ImagePrice1K *float64 `json:"image_price_1k"` + ImagePrice2K *float64 `json:"image_price_2k"` + ImagePrice4K *float64 `json:"image_price_4k"` + CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -168,6 +173,10 @@ type UsageLog struct { DurationMs *int `json:"duration_ms"` FirstTokenMs *int `json:"first_token_ms"` + // 图片生成字段 + ImageCount int `json:"image_count"` + ImageSize *string `json:"image_size"` + CreatedAt time.Time `json:"created_at"` User *User `json:"user,omitempty"` diff --git a/backend/internal/pkg/antigravity/gemini_types.go b/backend/internal/pkg/antigravity/gemini_types.go index 67f6c3e7..f688332f 100644 --- a/backend/internal/pkg/antigravity/gemini_types.go +++ b/backend/internal/pkg/antigravity/gemini_types.go @@ -67,6 +67,13 @@ type GeminiGenerationConfig struct { TopK *int `json:"topK,omitempty"` ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"` StopSequences []string `json:"stopSequences,omitempty"` + ImageConfig *GeminiImageConfig `json:"imageConfig,omitempty"` +} + +// GeminiImageConfig Gemini 图片生成配置(仅 gemini-3-pro-image 支持) +type GeminiImageConfig struct { + AspectRatio string `json:"aspectRatio,omitempty"` // "1:1", "16:9", "9:16", "4:3", "3:4" + ImageSize string `json:"imageSize,omitempty"` // "1K", "2K", "4K" } // GeminiThinkingConfig Gemini thinking 配置 diff --git a/backend/internal/repository/api_key_repo.go b/backend/internal/repository/api_key_repo.go index 530d86f7..4384bff5 100644 --- a/backend/internal/repository/api_key_repo.go +++ b/backend/internal/repository/api_key_repo.go @@ -321,6 +321,9 @@ func groupEntityToService(g *dbent.Group) *service.Group { DailyLimitUSD: g.DailyLimitUsd, WeeklyLimitUSD: g.WeeklyLimitUsd, MonthlyLimitUSD: g.MonthlyLimitUsd, + ImagePrice1K: g.ImagePrice1k, + ImagePrice2K: g.ImagePrice2k, + ImagePrice4K: g.ImagePrice4k, DefaultValidityDays: g.DefaultValidityDays, CreatedAt: g.CreatedAt, UpdatedAt: g.UpdatedAt, diff --git a/backend/internal/repository/group_repo.go b/backend/internal/repository/group_repo.go index c4597ce2..729c1404 100644 --- a/backend/internal/repository/group_repo.go +++ b/backend/internal/repository/group_repo.go @@ -43,6 +43,9 @@ func (r *groupRepository) Create(ctx context.Context, groupIn *service.Group) er SetNillableDailyLimitUsd(groupIn.DailyLimitUSD). SetNillableWeeklyLimitUsd(groupIn.WeeklyLimitUSD). SetNillableMonthlyLimitUsd(groupIn.MonthlyLimitUSD). + SetNillableImagePrice1k(groupIn.ImagePrice1K). + SetNillableImagePrice2k(groupIn.ImagePrice2K). + SetNillableImagePrice4k(groupIn.ImagePrice4K). SetDefaultValidityDays(groupIn.DefaultValidityDays) created, err := builder.Save(ctx) @@ -80,6 +83,9 @@ func (r *groupRepository) Update(ctx context.Context, groupIn *service.Group) er SetNillableDailyLimitUsd(groupIn.DailyLimitUSD). SetNillableWeeklyLimitUsd(groupIn.WeeklyLimitUSD). SetNillableMonthlyLimitUsd(groupIn.MonthlyLimitUSD). + SetNillableImagePrice1k(groupIn.ImagePrice1K). + SetNillableImagePrice2k(groupIn.ImagePrice2K). + SetNillableImagePrice4k(groupIn.ImagePrice4K). SetDefaultValidityDays(groupIn.DefaultValidityDays). Save(ctx) if err != nil { diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index aaa38f81..82d5e833 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -22,7 +22,7 @@ import ( "github.com/lib/pq" ) -const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, created_at" +const usageLogSelectColumns = "id, user_id, api_key_id, account_id, request_id, model, group_id, subscription_id, input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens, cache_creation_5m_tokens, cache_creation_1h_tokens, input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost, rate_multiplier, billing_type, stream, duration_ms, first_token_ms, image_count, image_size, created_at" type usageLogRepository struct { client *dbent.Client @@ -109,6 +109,8 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) stream, duration_ms, first_token_ms, + image_count, + image_size, created_at ) VALUES ( $1, $2, $3, $4, $5, @@ -116,7 +118,8 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, - $20, $21, $22, $23, $24, $25 + $20, $21, $22, $23, $24, + $25, $26, $27 ) ON CONFLICT (request_id, api_key_id) DO NOTHING RETURNING id, created_at @@ -126,6 +129,7 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) subscriptionID := nullInt64(log.SubscriptionID) duration := nullInt(log.DurationMs) firstToken := nullInt(log.FirstTokenMs) + imageSize := nullString(log.ImageSize) var requestIDArg any if requestID != "" { @@ -157,6 +161,8 @@ func (r *usageLogRepository) Create(ctx context.Context, log *service.UsageLog) log.Stream, duration, firstToken, + log.ImageCount, + imageSize, createdAt, } if err := scanSingleRow(ctx, sqlq, query, args, &log.ID, &log.CreatedAt); err != nil { @@ -1789,6 +1795,8 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e stream bool durationMs sql.NullInt64 firstTokenMs sql.NullInt64 + imageCount int + imageSize sql.NullString createdAt time.Time ) @@ -1818,6 +1826,8 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e &stream, &durationMs, &firstTokenMs, + &imageCount, + &imageSize, &createdAt, ); err != nil { return nil, err @@ -1844,6 +1854,7 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e RateMultiplier: rateMultiplier, BillingType: int8(billingType), Stream: stream, + ImageCount: imageCount, CreatedAt: createdAt, } @@ -1866,6 +1877,9 @@ func scanUsageLog(scanner interface{ Scan(...any) error }) (*service.UsageLog, e value := int(firstTokenMs.Int64) log.FirstTokenMs = &value } + if imageSize.Valid { + log.ImageSize = &imageSize.String + } return log, nil } @@ -1938,6 +1952,13 @@ func nullInt(v *int) sql.NullInt64 { return sql.NullInt64{Int64: int64(*v), Valid: true} } +func nullString(v *string) sql.NullString { + if v == nil || *v == "" { + return sql.NullString{} + } + return sql.NullString{String: *v, Valid: true} +} + func setToSlice(set map[int64]struct{}) []int64 { out := make([]int64, 0, len(set)) for id := range set { diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index a88e2b4e..6f66f067 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -98,6 +98,10 @@ type CreateGroupInput struct { DailyLimitUSD *float64 // 日限额 (USD) WeeklyLimitUSD *float64 // 周限额 (USD) MonthlyLimitUSD *float64 // 月限额 (USD) + // 图片生成计费配置(仅 antigravity 平台使用) + ImagePrice1K *float64 + ImagePrice2K *float64 + ImagePrice4K *float64 } type UpdateGroupInput struct { @@ -111,6 +115,10 @@ type UpdateGroupInput struct { DailyLimitUSD *float64 // 日限额 (USD) WeeklyLimitUSD *float64 // 周限额 (USD) MonthlyLimitUSD *float64 // 月限额 (USD) + // 图片生成计费配置(仅 antigravity 平台使用) + ImagePrice1K *float64 + ImagePrice2K *float64 + ImagePrice4K *float64 } type CreateAccountInput struct { @@ -507,6 +515,9 @@ func (s *adminServiceImpl) CreateGroup(ctx context.Context, input *CreateGroupIn DailyLimitUSD: dailyLimit, WeeklyLimitUSD: weeklyLimit, MonthlyLimitUSD: monthlyLimit, + ImagePrice1K: input.ImagePrice1K, + ImagePrice2K: input.ImagePrice2K, + ImagePrice4K: input.ImagePrice4K, } if err := s.groupRepo.Create(ctx, group); err != nil { return nil, err @@ -561,6 +572,16 @@ func (s *adminServiceImpl) UpdateGroup(ctx context.Context, id int64, input *Upd if input.MonthlyLimitUSD != nil { group.MonthlyLimitUSD = normalizeLimit(input.MonthlyLimitUSD) } + // 图片生成计费配置 + if input.ImagePrice1K != nil { + group.ImagePrice1K = input.ImagePrice1K + } + if input.ImagePrice2K != nil { + group.ImagePrice2K = input.ImagePrice2K + } + if input.ImagePrice4K != nil { + group.ImagePrice4K = input.ImagePrice4K + } if err := s.groupRepo.Update(ctx, group); err != nil { return nil, err diff --git a/backend/internal/service/admin_service_group_test.go b/backend/internal/service/admin_service_group_test.go new file mode 100644 index 00000000..3171de11 --- /dev/null +++ b/backend/internal/service/admin_service_group_test.go @@ -0,0 +1,197 @@ +//go:build unit + +package service + +import ( + "context" + "testing" + + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/stretchr/testify/require" +) + +// groupRepoStubForAdmin 用于测试 AdminService 的 GroupRepository Stub +type groupRepoStubForAdmin struct { + created *Group // 记录 Create 调用的参数 + updated *Group // 记录 Update 调用的参数 + getByID *Group // GetByID 返回值 + getErr error // GetByID 返回的错误 +} + +func (s *groupRepoStubForAdmin) Create(_ context.Context, g *Group) error { + s.created = g + return nil +} + +func (s *groupRepoStubForAdmin) Update(_ context.Context, g *Group) error { + s.updated = g + return nil +} + +func (s *groupRepoStubForAdmin) GetByID(_ context.Context, _ int64) (*Group, error) { + if s.getErr != nil { + return nil, s.getErr + } + return s.getByID, nil +} + +func (s *groupRepoStubForAdmin) Delete(_ context.Context, _ int64) error { + panic("unexpected Delete call") +} + +func (s *groupRepoStubForAdmin) DeleteCascade(_ context.Context, _ int64) ([]int64, error) { + panic("unexpected DeleteCascade call") +} + +func (s *groupRepoStubForAdmin) List(_ context.Context, _ pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) { + panic("unexpected List call") +} + +func (s *groupRepoStubForAdmin) ListWithFilters(_ context.Context, _ pagination.PaginationParams, _, _ string, _ *bool) ([]Group, *pagination.PaginationResult, error) { + panic("unexpected ListWithFilters call") +} + +func (s *groupRepoStubForAdmin) ListActive(_ context.Context) ([]Group, error) { + panic("unexpected ListActive call") +} + +func (s *groupRepoStubForAdmin) ListActiveByPlatform(_ context.Context, _ string) ([]Group, error) { + panic("unexpected ListActiveByPlatform call") +} + +func (s *groupRepoStubForAdmin) ExistsByName(_ context.Context, _ string) (bool, error) { + panic("unexpected ExistsByName call") +} + +func (s *groupRepoStubForAdmin) GetAccountCount(_ context.Context, _ int64) (int64, error) { + panic("unexpected GetAccountCount call") +} + +func (s *groupRepoStubForAdmin) DeleteAccountGroupsByGroupID(_ context.Context, _ int64) (int64, error) { + panic("unexpected DeleteAccountGroupsByGroupID call") +} + +// TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递 +func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) { + repo := &groupRepoStubForAdmin{} + svc := &adminServiceImpl{groupRepo: repo} + + price1K := 0.10 + price2K := 0.15 + price4K := 0.30 + + input := &CreateGroupInput{ + Name: "test-group", + Description: "Test group", + Platform: PlatformAntigravity, + RateMultiplier: 1.0, + ImagePrice1K: &price1K, + ImagePrice2K: &price2K, + ImagePrice4K: &price4K, + } + + group, err := svc.CreateGroup(context.Background(), input) + require.NoError(t, err) + require.NotNil(t, group) + + // 验证 repo 收到了正确的字段 + require.NotNil(t, repo.created) + require.NotNil(t, repo.created.ImagePrice1K) + require.NotNil(t, repo.created.ImagePrice2K) + require.NotNil(t, repo.created.ImagePrice4K) + require.InDelta(t, 0.10, *repo.created.ImagePrice1K, 0.0001) + require.InDelta(t, 0.15, *repo.created.ImagePrice2K, 0.0001) + require.InDelta(t, 0.30, *repo.created.ImagePrice4K, 0.0001) +} + +// TestAdminService_CreateGroup_NilImagePricing 测试 ImagePrice 为 nil 时正常创建 +func TestAdminService_CreateGroup_NilImagePricing(t *testing.T) { + repo := &groupRepoStubForAdmin{} + svc := &adminServiceImpl{groupRepo: repo} + + input := &CreateGroupInput{ + Name: "test-group", + Description: "Test group", + Platform: PlatformAntigravity, + RateMultiplier: 1.0, + // ImagePrice 字段全部为 nil + } + + group, err := svc.CreateGroup(context.Background(), input) + require.NoError(t, err) + require.NotNil(t, group) + + // 验证 ImagePrice 字段为 nil + require.NotNil(t, repo.created) + require.Nil(t, repo.created.ImagePrice1K) + require.Nil(t, repo.created.ImagePrice2K) + require.Nil(t, repo.created.ImagePrice4K) +} + +// TestAdminService_UpdateGroup_WithImagePricing 测试更新分组时 ImagePrice 字段正确更新 +func TestAdminService_UpdateGroup_WithImagePricing(t *testing.T) { + existingGroup := &Group{ + ID: 1, + Name: "existing-group", + Platform: PlatformAntigravity, + Status: StatusActive, + } + repo := &groupRepoStubForAdmin{getByID: existingGroup} + svc := &adminServiceImpl{groupRepo: repo} + + price1K := 0.12 + price2K := 0.18 + price4K := 0.36 + + input := &UpdateGroupInput{ + ImagePrice1K: &price1K, + ImagePrice2K: &price2K, + ImagePrice4K: &price4K, + } + + group, err := svc.UpdateGroup(context.Background(), 1, input) + require.NoError(t, err) + require.NotNil(t, group) + + // 验证 repo 收到了更新后的字段 + require.NotNil(t, repo.updated) + require.NotNil(t, repo.updated.ImagePrice1K) + require.NotNil(t, repo.updated.ImagePrice2K) + require.NotNil(t, repo.updated.ImagePrice4K) + require.InDelta(t, 0.12, *repo.updated.ImagePrice1K, 0.0001) + require.InDelta(t, 0.18, *repo.updated.ImagePrice2K, 0.0001) + require.InDelta(t, 0.36, *repo.updated.ImagePrice4K, 0.0001) +} + +// TestAdminService_UpdateGroup_PartialImagePricing 测试仅更新部分 ImagePrice 字段 +func TestAdminService_UpdateGroup_PartialImagePricing(t *testing.T) { + oldPrice2K := 0.15 + existingGroup := &Group{ + ID: 1, + Name: "existing-group", + Platform: PlatformAntigravity, + Status: StatusActive, + ImagePrice2K: &oldPrice2K, // 已有 2K 价格 + } + repo := &groupRepoStubForAdmin{getByID: existingGroup} + svc := &adminServiceImpl{groupRepo: repo} + + // 只更新 1K 价格 + price1K := 0.10 + input := &UpdateGroupInput{ + ImagePrice1K: &price1K, + // ImagePrice2K 和 ImagePrice4K 为 nil,不更新 + } + + group, err := svc.UpdateGroup(context.Background(), 1, input) + require.NoError(t, err) + require.NotNil(t, group) + + // 验证:1K 被更新,2K 保持原值,4K 仍为 nil + require.NotNil(t, repo.updated) + require.NotNil(t, repo.updated.ImagePrice1K) + require.InDelta(t, 0.10, *repo.updated.ImagePrice1K, 0.0001) + require.NotNil(t, repo.updated.ImagePrice2K) + require.InDelta(t, 0.15, *repo.updated.ImagePrice2K, 0.0001) // 原值保持 + require.Nil(t, repo.updated.ImagePrice4K) +} diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go index cbe78ea5..d77ffaa6 100644 --- a/backend/internal/service/antigravity_gateway_service.go +++ b/backend/internal/service/antigravity_gateway_service.go @@ -652,6 +652,9 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co return nil, s.writeGoogleError(c, http.StatusBadRequest, "Request body is empty") } + // 解析请求以获取 image_size(用于图片计费) + imageSize := s.extractImageSize(body) + switch action { case "generateContent", "streamGenerateContent": // ok @@ -832,6 +835,13 @@ handleSuccess: usage = &ClaudeUsage{} } + // 判断是否为图片生成模型 + imageCount := 0 + if isImageGenerationModel(mappedModel) { + // 图片模型按次计费,默认 1 张图片 + imageCount = 1 + } + return &ForwardResult{ RequestID: requestID, Usage: *usage, @@ -839,6 +849,8 @@ handleSuccess: Stream: stream, Duration: time.Since(startTime), FirstTokenMs: firstTokenMs, + ImageCount: imageCount, + ImageSize: imageSize, }, nil } @@ -1161,3 +1173,27 @@ func (s *AntigravityGatewayService) handleClaudeStreamingResponse(c *gin.Context return &antigravityStreamResult{usage: convertUsage(agUsage), firstTokenMs: firstTokenMs}, nil } + +// extractImageSize 从 Gemini 请求中提取 image_size 参数 +func (s *AntigravityGatewayService) extractImageSize(body []byte) string { + var req antigravity.GeminiRequest + if err := json.Unmarshal(body, &req); err != nil { + return "2K" // 默认 2K + } + + if req.GenerationConfig != nil && req.GenerationConfig.ImageConfig != nil { + size := strings.ToUpper(strings.TrimSpace(req.GenerationConfig.ImageConfig.ImageSize)) + if size == "1K" || size == "2K" || size == "4K" { + return size + } + } + + return "2K" // 默认 2K +} + +// isImageGenerationModel 判断模型是否为图片生成模型 +func isImageGenerationModel(model string) bool { + modelLower := strings.ToLower(model) + return strings.Contains(modelLower, "gemini-3-pro-image") || + strings.Contains(modelLower, "gemini-2.5-flash-image") +} diff --git a/backend/internal/service/antigravity_image_test.go b/backend/internal/service/antigravity_image_test.go new file mode 100644 index 00000000..1579c790 --- /dev/null +++ b/backend/internal/service/antigravity_image_test.go @@ -0,0 +1,120 @@ +//go:build unit + +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestIsImageGenerationModel_GeminiProImage 测试 gemini-3-pro-image 识别 +func TestIsImageGenerationModel_GeminiProImage(t *testing.T) { + require.True(t, isImageGenerationModel("gemini-3-pro-image")) + require.True(t, isImageGenerationModel("gemini-3-pro-image-preview")) + require.True(t, isImageGenerationModel("models/gemini-3-pro-image")) +} + +// TestIsImageGenerationModel_GeminiFlashImage 测试 gemini-2.5-flash-image 识别 +func TestIsImageGenerationModel_GeminiFlashImage(t *testing.T) { + require.True(t, isImageGenerationModel("gemini-2.5-flash-image")) + require.True(t, isImageGenerationModel("gemini-2.5-flash-image-preview")) +} + +// TestIsImageGenerationModel_RegularModel 测试普通模型不被识别为图片模型 +func TestIsImageGenerationModel_RegularModel(t *testing.T) { + require.False(t, isImageGenerationModel("claude-3-opus")) + require.False(t, isImageGenerationModel("claude-sonnet-4-20250514")) + require.False(t, isImageGenerationModel("gpt-4o")) + require.False(t, isImageGenerationModel("gemini-2.5-pro")) // 非图片模型 + require.False(t, isImageGenerationModel("gemini-2.5-flash")) +} + +// TestIsImageGenerationModel_CaseInsensitive 测试大小写不敏感 +func TestIsImageGenerationModel_CaseInsensitive(t *testing.T) { + require.True(t, isImageGenerationModel("GEMINI-3-PRO-IMAGE")) + require.True(t, isImageGenerationModel("Gemini-3-Pro-Image")) + require.True(t, isImageGenerationModel("GEMINI-2.5-FLASH-IMAGE")) +} + +// TestExtractImageSize_ValidSizes 测试有效尺寸解析 +func TestExtractImageSize_ValidSizes(t *testing.T) { + svc := &AntigravityGatewayService{} + + // 1K + body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":"1K"}}}`) + require.Equal(t, "1K", svc.extractImageSize(body)) + + // 2K + body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"2K"}}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) + + // 4K + body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"4K"}}}`) + require.Equal(t, "4K", svc.extractImageSize(body)) +} + +// TestExtractImageSize_CaseInsensitive 测试大小写不敏感 +func TestExtractImageSize_CaseInsensitive(t *testing.T) { + svc := &AntigravityGatewayService{} + + body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":"1k"}}}`) + require.Equal(t, "1K", svc.extractImageSize(body)) + + body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"4k"}}}`) + require.Equal(t, "4K", svc.extractImageSize(body)) +} + +// TestExtractImageSize_Default 测试无 imageConfig 返回默认 2K +func TestExtractImageSize_Default(t *testing.T) { + svc := &AntigravityGatewayService{} + + // 无 generationConfig + body := []byte(`{"contents":[]}`) + require.Equal(t, "2K", svc.extractImageSize(body)) + + // 有 generationConfig 但无 imageConfig + body = []byte(`{"generationConfig":{"temperature":0.7}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) + + // 有 imageConfig 但无 imageSize + body = []byte(`{"generationConfig":{"imageConfig":{}}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) +} + +// TestExtractImageSize_InvalidJSON 测试非法 JSON 返回默认 2K +func TestExtractImageSize_InvalidJSON(t *testing.T) { + svc := &AntigravityGatewayService{} + + body := []byte(`not valid json`) + require.Equal(t, "2K", svc.extractImageSize(body)) + + body = []byte(`{"broken":`) + require.Equal(t, "2K", svc.extractImageSize(body)) +} + +// TestExtractImageSize_EmptySize 测试空 imageSize 返回默认 2K +func TestExtractImageSize_EmptySize(t *testing.T) { + svc := &AntigravityGatewayService{} + + body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":""}}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) + + // 空格 + body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":" "}}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) +} + +// TestExtractImageSize_InvalidSize 测试无效尺寸返回默认 2K +func TestExtractImageSize_InvalidSize(t *testing.T) { + svc := &AntigravityGatewayService{} + + body := []byte(`{"generationConfig":{"imageConfig":{"imageSize":"3K"}}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) + + body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"8K"}}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) + + body = []byte(`{"generationConfig":{"imageConfig":{"imageSize":"invalid"}}}`) + require.Equal(t, "2K", svc.extractImageSize(body)) +} diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index a2254744..f2afc343 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -295,3 +295,88 @@ func (s *BillingService) ForceUpdatePricing() error { } return fmt.Errorf("pricing service not initialized") } + +// ImagePriceConfig 图片计费配置 +type ImagePriceConfig struct { + Price1K *float64 // 1K 尺寸价格(nil 表示使用默认值) + Price2K *float64 // 2K 尺寸价格(nil 表示使用默认值) + Price4K *float64 // 4K 尺寸价格(nil 表示使用默认值) +} + +// CalculateImageCost 计算图片生成费用 +// model: 请求的模型名称(用于获取 LiteLLM 默认价格) +// imageSize: 图片尺寸 "1K", "2K", "4K" +// imageCount: 生成的图片数量 +// groupConfig: 分组配置的价格(可能为 nil,表示使用默认值) +// rateMultiplier: 费率倍数 +func (s *BillingService) CalculateImageCost(model string, imageSize string, imageCount int, groupConfig *ImagePriceConfig, rateMultiplier float64) *CostBreakdown { + if imageCount <= 0 { + return &CostBreakdown{} + } + + // 获取单价 + unitPrice := s.getImageUnitPrice(model, imageSize, groupConfig) + + // 计算总费用 + totalCost := unitPrice * float64(imageCount) + + // 应用倍率 + if rateMultiplier <= 0 { + rateMultiplier = 1.0 + } + actualCost := totalCost * rateMultiplier + + return &CostBreakdown{ + TotalCost: totalCost, + ActualCost: actualCost, + } +} + +// getImageUnitPrice 获取图片单价 +func (s *BillingService) getImageUnitPrice(model string, imageSize string, groupConfig *ImagePriceConfig) float64 { + // 优先使用分组配置的价格 + if groupConfig != nil { + switch imageSize { + case "1K": + if groupConfig.Price1K != nil { + return *groupConfig.Price1K + } + case "2K": + if groupConfig.Price2K != nil { + return *groupConfig.Price2K + } + case "4K": + if groupConfig.Price4K != nil { + return *groupConfig.Price4K + } + } + } + + // 回退到 LiteLLM 默认价格 + return s.getDefaultImagePrice(model, imageSize) +} + +// getDefaultImagePrice 获取 LiteLLM 默认图片价格 +func (s *BillingService) getDefaultImagePrice(model string, imageSize string) float64 { + basePrice := 0.0 + + // 从 PricingService 获取 output_cost_per_image + if s.pricingService != nil { + pricing := s.pricingService.GetModelPricing(model) + if pricing != nil && pricing.OutputCostPerImage > 0 { + basePrice = pricing.OutputCostPerImage + } + } + + // 如果没有找到价格,使用硬编码默认值($0.134,来自 gemini-3-pro-image-preview) + if basePrice <= 0 { + basePrice = 0.134 + } + + // 4K 尺寸翻倍 + if imageSize == "4K" { + return basePrice * 2 + } + + return basePrice +} diff --git a/backend/internal/service/billing_service_image_test.go b/backend/internal/service/billing_service_image_test.go new file mode 100644 index 00000000..18a6b74d --- /dev/null +++ b/backend/internal/service/billing_service_image_test.go @@ -0,0 +1,149 @@ +//go:build unit + +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestCalculateImageCost_DefaultPricing 测试无分组配置时使用默认价格 +func TestCalculateImageCost_DefaultPricing(t *testing.T) { + svc := &BillingService{} // pricingService 为 nil,使用硬编码默认值 + + // 2K 尺寸,默认价格 $0.134 + cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 1.0) + require.InDelta(t, 0.134, cost.TotalCost, 0.0001) + require.InDelta(t, 0.134, cost.ActualCost, 0.0001) + + // 多张图片 + cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 3, nil, 1.0) + require.InDelta(t, 0.402, cost.TotalCost, 0.0001) +} + +// TestCalculateImageCost_GroupCustomPricing 测试分组自定义价格 +func TestCalculateImageCost_GroupCustomPricing(t *testing.T) { + svc := &BillingService{} + + price1K := 0.10 + price2K := 0.15 + price4K := 0.30 + groupConfig := &ImagePriceConfig{ + Price1K: &price1K, + Price2K: &price2K, + Price4K: &price4K, + } + + // 1K 使用分组价格 + cost := svc.CalculateImageCost("gemini-3-pro-image", "1K", 2, groupConfig, 1.0) + require.InDelta(t, 0.20, cost.TotalCost, 0.0001) + + // 2K 使用分组价格 + cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, groupConfig, 1.0) + require.InDelta(t, 0.15, cost.TotalCost, 0.0001) + + // 4K 使用分组价格 + cost = svc.CalculateImageCost("gemini-3-pro-image", "4K", 1, groupConfig, 1.0) + require.InDelta(t, 0.30, cost.TotalCost, 0.0001) +} + +// TestCalculateImageCost_4KDoublePrice 测试 4K 默认价格翻倍 +func TestCalculateImageCost_4KDoublePrice(t *testing.T) { + svc := &BillingService{} + + // 4K 尺寸,默认价格翻倍 $0.134 * 2 = $0.268 + cost := svc.CalculateImageCost("gemini-3-pro-image", "4K", 1, nil, 1.0) + require.InDelta(t, 0.268, cost.TotalCost, 0.0001) +} + +// TestCalculateImageCost_RateMultiplier 测试费率倍数 +func TestCalculateImageCost_RateMultiplier(t *testing.T) { + svc := &BillingService{} + + // 费率倍数 1.5x + cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 1.5) + require.InDelta(t, 0.134, cost.TotalCost, 0.0001) // TotalCost 不变 + require.InDelta(t, 0.201, cost.ActualCost, 0.0001) // ActualCost = 0.134 * 1.5 + + // 费率倍数 2.0x + cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 2, nil, 2.0) + require.InDelta(t, 0.268, cost.TotalCost, 0.0001) + require.InDelta(t, 0.536, cost.ActualCost, 0.0001) +} + +// TestCalculateImageCost_ZeroCount 测试 imageCount=0 +func TestCalculateImageCost_ZeroCount(t *testing.T) { + svc := &BillingService{} + + cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 0, nil, 1.0) + require.Equal(t, 0.0, cost.TotalCost) + require.Equal(t, 0.0, cost.ActualCost) +} + +// TestCalculateImageCost_NegativeCount 测试 imageCount=-1 +func TestCalculateImageCost_NegativeCount(t *testing.T) { + svc := &BillingService{} + + cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", -1, nil, 1.0) + require.Equal(t, 0.0, cost.TotalCost) + require.Equal(t, 0.0, cost.ActualCost) +} + +// TestCalculateImageCost_ZeroRateMultiplier 测试费率倍数为 0 时默认使用 1.0 +func TestCalculateImageCost_ZeroRateMultiplier(t *testing.T) { + svc := &BillingService{} + + cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 0) + require.InDelta(t, 0.134, cost.TotalCost, 0.0001) + require.InDelta(t, 0.134, cost.ActualCost, 0.0001) // 0 倍率当作 1.0 处理 +} + +// TestGetImageUnitPrice_GroupPriorityOverDefault 测试分组价格优先于默认价格 +func TestGetImageUnitPrice_GroupPriorityOverDefault(t *testing.T) { + svc := &BillingService{} + + price2K := 0.20 + groupConfig := &ImagePriceConfig{ + Price2K: &price2K, + } + + // 分组配置了 2K 价格,应该使用分组价格而不是默认的 $0.134 + cost := svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, groupConfig, 1.0) + require.InDelta(t, 0.20, cost.TotalCost, 0.0001) +} + +// TestGetImageUnitPrice_PartialGroupConfig 测试分组部分配置时回退默认 +func TestGetImageUnitPrice_PartialGroupConfig(t *testing.T) { + svc := &BillingService{} + + // 只配置 1K 价格 + price1K := 0.10 + groupConfig := &ImagePriceConfig{ + Price1K: &price1K, + } + + // 1K 使用分组价格 + cost := svc.CalculateImageCost("gemini-3-pro-image", "1K", 1, groupConfig, 1.0) + require.InDelta(t, 0.10, cost.TotalCost, 0.0001) + + // 2K 回退默认价格 $0.134 + cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, groupConfig, 1.0) + require.InDelta(t, 0.134, cost.TotalCost, 0.0001) + + // 4K 回退默认价格 $0.268 (翻倍) + cost = svc.CalculateImageCost("gemini-3-pro-image", "4K", 1, groupConfig, 1.0) + require.InDelta(t, 0.268, cost.TotalCost, 0.0001) +} + +// TestGetDefaultImagePrice_FallbackHardcoded 测试 PricingService 无数据时使用硬编码默认值 +func TestGetDefaultImagePrice_FallbackHardcoded(t *testing.T) { + svc := &BillingService{} // pricingService 为 nil + + // 1K 和 2K 使用相同的默认价格 $0.134 + cost := svc.CalculateImageCost("gemini-3-pro-image", "1K", 1, nil, 1.0) + require.InDelta(t, 0.134, cost.TotalCost, 0.0001) + + cost = svc.CalculateImageCost("gemini-3-pro-image", "2K", 1, nil, 1.0) + require.InDelta(t, 0.134, cost.TotalCost, 0.0001) +} diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 97e4c2e8..c57c8e0f 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -100,6 +100,10 @@ type ForwardResult struct { Stream bool Duration time.Duration FirstTokenMs *int // 首字时间(流式请求) + + // 图片生成计费字段(仅 gemini-3-pro-image 使用) + ImageCount int // 生成的图片数量 + ImageSize string // 图片尺寸 "1K", "2K", "4K" } // UpstreamFailoverError indicates an upstream error that should trigger account failover. @@ -1794,25 +1798,40 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu account := input.Account subscription := input.Subscription - // 计算费用 - tokens := UsageTokens{ - InputTokens: result.Usage.InputTokens, - OutputTokens: result.Usage.OutputTokens, - CacheCreationTokens: result.Usage.CacheCreationInputTokens, - CacheReadTokens: result.Usage.CacheReadInputTokens, - } - // 获取费率倍数 multiplier := s.cfg.Default.RateMultiplier if apiKey.GroupID != nil && apiKey.Group != nil { multiplier = apiKey.Group.RateMultiplier } - cost, err := s.billingService.CalculateCost(result.Model, tokens, multiplier) - if err != nil { - log.Printf("Calculate cost failed: %v", err) - // 使用默认费用继续 - cost = &CostBreakdown{ActualCost: 0} + var cost *CostBreakdown + + // 根据请求类型选择计费方式 + if result.ImageCount > 0 { + // 图片生成计费 + var groupConfig *ImagePriceConfig + if apiKey.Group != nil { + groupConfig = &ImagePriceConfig{ + Price1K: apiKey.Group.ImagePrice1K, + Price2K: apiKey.Group.ImagePrice2K, + Price4K: apiKey.Group.ImagePrice4K, + } + } + cost = s.billingService.CalculateImageCost(result.Model, result.ImageSize, result.ImageCount, groupConfig, multiplier) + } else { + // Token 计费 + tokens := UsageTokens{ + InputTokens: result.Usage.InputTokens, + OutputTokens: result.Usage.OutputTokens, + CacheCreationTokens: result.Usage.CacheCreationInputTokens, + CacheReadTokens: result.Usage.CacheReadInputTokens, + } + var err error + cost, err = s.billingService.CalculateCost(result.Model, tokens, multiplier) + if err != nil { + log.Printf("Calculate cost failed: %v", err) + cost = &CostBreakdown{ActualCost: 0} + } } // 判断计费方式:订阅模式 vs 余额模式 @@ -1824,6 +1843,10 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu // 创建使用日志 durationMs := int(result.Duration.Milliseconds()) + var imageSize *string + if result.ImageSize != "" { + imageSize = &result.ImageSize + } usageLog := &UsageLog{ UserID: user.ID, APIKeyID: apiKey.ID, @@ -1845,6 +1868,8 @@ func (s *GatewayService) RecordUsage(ctx context.Context, input *RecordUsageInpu Stream: result.Stream, DurationMs: &durationMs, FirstTokenMs: result.FirstTokenMs, + ImageCount: result.ImageCount, + ImageSize: imageSize, CreatedAt: time.Now(), } diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index 7d6f407d..01b6b513 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -17,6 +17,11 @@ type Group struct { MonthlyLimitUSD *float64 DefaultValidityDays int + // 图片生成计费配置(antigravity 和 gemini 平台使用) + ImagePrice1K *float64 + ImagePrice2K *float64 + ImagePrice4K *float64 + CreatedAt time.Time UpdatedAt time.Time @@ -47,3 +52,19 @@ func (g *Group) HasWeeklyLimit() bool { func (g *Group) HasMonthlyLimit() bool { return g.MonthlyLimitUSD != nil && *g.MonthlyLimitUSD > 0 } + +// GetImagePrice 根据 image_size 返回对应的图片生成价格 +// 如果分组未配置价格,返回 nil(调用方应使用默认值) +func (g *Group) GetImagePrice(imageSize string) *float64 { + switch imageSize { + case "1K": + return g.ImagePrice1K + case "2K": + return g.ImagePrice2K + case "4K": + return g.ImagePrice4K + default: + // 未知尺寸默认按 2K 计费 + return g.ImagePrice2K + } +} diff --git a/backend/internal/service/group_test.go b/backend/internal/service/group_test.go new file mode 100644 index 00000000..a0f9672c --- /dev/null +++ b/backend/internal/service/group_test.go @@ -0,0 +1,92 @@ +//go:build unit + +package service + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestGroup_GetImagePrice_1K 测试 1K 尺寸返回正确价格 +func TestGroup_GetImagePrice_1K(t *testing.T) { + price := 0.10 + group := &Group{ + ImagePrice1K: &price, + } + + result := group.GetImagePrice("1K") + require.NotNil(t, result) + require.InDelta(t, 0.10, *result, 0.0001) +} + +// TestGroup_GetImagePrice_2K 测试 2K 尺寸返回正确价格 +func TestGroup_GetImagePrice_2K(t *testing.T) { + price := 0.15 + group := &Group{ + ImagePrice2K: &price, + } + + result := group.GetImagePrice("2K") + require.NotNil(t, result) + require.InDelta(t, 0.15, *result, 0.0001) +} + +// TestGroup_GetImagePrice_4K 测试 4K 尺寸返回正确价格 +func TestGroup_GetImagePrice_4K(t *testing.T) { + price := 0.30 + group := &Group{ + ImagePrice4K: &price, + } + + result := group.GetImagePrice("4K") + require.NotNil(t, result) + require.InDelta(t, 0.30, *result, 0.0001) +} + +// TestGroup_GetImagePrice_UnknownSize 测试未知尺寸回退 2K +func TestGroup_GetImagePrice_UnknownSize(t *testing.T) { + price2K := 0.15 + group := &Group{ + ImagePrice2K: &price2K, + } + + // 未知尺寸 "3K" 应该回退到 2K + result := group.GetImagePrice("3K") + require.NotNil(t, result) + require.InDelta(t, 0.15, *result, 0.0001) + + // 空字符串也回退到 2K + result = group.GetImagePrice("") + require.NotNil(t, result) + require.InDelta(t, 0.15, *result, 0.0001) +} + +// TestGroup_GetImagePrice_NilValues 测试未配置时返回 nil +func TestGroup_GetImagePrice_NilValues(t *testing.T) { + group := &Group{ + // 所有 ImagePrice 字段都是 nil + } + + require.Nil(t, group.GetImagePrice("1K")) + require.Nil(t, group.GetImagePrice("2K")) + require.Nil(t, group.GetImagePrice("4K")) + require.Nil(t, group.GetImagePrice("unknown")) +} + +// TestGroup_GetImagePrice_PartialConfig 测试部分配置 +func TestGroup_GetImagePrice_PartialConfig(t *testing.T) { + price1K := 0.10 + group := &Group{ + ImagePrice1K: &price1K, + // ImagePrice2K 和 ImagePrice4K 未配置 + } + + result := group.GetImagePrice("1K") + require.NotNil(t, result) + require.InDelta(t, 0.10, *result, 0.0001) + + // 2K 和 4K 返回 nil + require.Nil(t, group.GetImagePrice("2K")) + require.Nil(t, group.GetImagePrice("4K")) +} diff --git a/backend/internal/service/pricing_service.go b/backend/internal/service/pricing_service.go index bb050d0a..4a56985f 100644 --- a/backend/internal/service/pricing_service.go +++ b/backend/internal/service/pricing_service.go @@ -33,6 +33,7 @@ type LiteLLMModelPricing struct { LiteLLMProvider string `json:"litellm_provider"` Mode string `json:"mode"` SupportsPromptCaching bool `json:"supports_prompt_caching"` + OutputCostPerImage float64 `json:"output_cost_per_image"` // 图片生成模型每张图片价格 } // PricingRemoteClient 远程价格数据获取接口 @@ -50,6 +51,7 @@ type LiteLLMRawEntry struct { LiteLLMProvider string `json:"litellm_provider"` Mode string `json:"mode"` SupportsPromptCaching bool `json:"supports_prompt_caching"` + OutputCostPerImage *float64 `json:"output_cost_per_image"` } // PricingService 动态价格服务 @@ -299,6 +301,9 @@ func (s *PricingService) parsePricingData(body []byte) (map[string]*LiteLLMModel if entry.CacheReadInputTokenCost != nil { pricing.CacheReadInputTokenCost = *entry.CacheReadInputTokenCost } + if entry.OutputCostPerImage != nil { + pricing.OutputCostPerImage = *entry.OutputCostPerImage + } result[modelName] = pricing } diff --git a/backend/internal/service/usage_log.go b/backend/internal/service/usage_log.go index ed0a8eb7..255f0440 100644 --- a/backend/internal/service/usage_log.go +++ b/backend/internal/service/usage_log.go @@ -39,6 +39,10 @@ type UsageLog struct { DurationMs *int FirstTokenMs *int + // 图片生成字段 + ImageCount int + ImageSize *string + CreatedAt time.Time User *User diff --git a/backend/migrations/028_group_image_pricing.sql b/backend/migrations/028_group_image_pricing.sql new file mode 100644 index 00000000..19961d1c --- /dev/null +++ b/backend/migrations/028_group_image_pricing.sql @@ -0,0 +1,10 @@ +-- 为 Antigravity 分组添加图片生成计费配置 +-- 支持 gemini-3-pro-image 模型的 1K/2K/4K 分辨率按次计费 + +ALTER TABLE groups ADD COLUMN IF NOT EXISTS image_price_1k DECIMAL(20,8); +ALTER TABLE groups ADD COLUMN IF NOT EXISTS image_price_2k DECIMAL(20,8); +ALTER TABLE groups ADD COLUMN IF NOT EXISTS image_price_4k DECIMAL(20,8); + +COMMENT ON COLUMN groups.image_price_1k IS '1K 分辨率图片生成单价 (USD),仅 antigravity 平台使用'; +COMMENT ON COLUMN groups.image_price_2k IS '2K 分辨率图片生成单价 (USD),仅 antigravity 平台使用'; +COMMENT ON COLUMN groups.image_price_4k IS '4K 分辨率图片生成单价 (USD),仅 antigravity 平台使用'; diff --git a/backend/migrations/029_usage_log_image_fields.sql b/backend/migrations/029_usage_log_image_fields.sql new file mode 100644 index 00000000..16304d24 --- /dev/null +++ b/backend/migrations/029_usage_log_image_fields.sql @@ -0,0 +1,5 @@ +-- 为使用日志添加图片生成统计字段 +-- 用于记录 gemini-3-pro-image 等图片生成模型的使用情况 + +ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_count INT DEFAULT 0; +ALTER TABLE usage_logs ADD COLUMN IF NOT EXISTS image_size VARCHAR(10); diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 19a5a1f6..afae7c82 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -403,7 +403,8 @@ export default { exportExcelFailed: 'Failed to export usage data', billingType: 'Billing', balance: 'Balance', - subscription: 'Subscription' + subscription: 'Subscription', + imageUnit: ' images' }, // Redeem @@ -811,6 +812,10 @@ export default { defaultValidityDays: 'Default Validity (Days)', validityHint: 'Number of days the subscription is valid when assigned to a user', noLimit: 'No limit' + }, + imagePricing: { + title: 'Image Generation Pricing', + description: 'Configure pricing for gemini-3-pro-image model. Leave empty to use default prices.' } }, diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 8aef9187..fc6bd8dd 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -399,7 +399,8 @@ export default { exportExcelFailed: '使用数据导出失败', billingType: '消费类型', balance: '余额', - subscription: '订阅' + subscription: '订阅', + imageUnit: '张' }, // Redeem @@ -900,6 +901,10 @@ export default { defaultValidityDays: '默认有效期(天)', validityHint: '分配给用户时订阅的有效天数', noLimit: '无限制' + }, + imagePricing: { + title: '图片生成计费', + description: '配置 gemini-3-pro-image 模型的图片生成价格,留空则使用默认价格' } }, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 04db3731..7aba5601 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -239,6 +239,10 @@ export interface Group { daily_limit_usd: number | null weekly_limit_usd: number | null monthly_limit_usd: number | null + // 图片生成计费配置(仅 antigravity 平台使用) + image_price_1k: number | null + image_price_2k: number | null + image_price_4k: number | null account_count?: number created_at: string updated_at: string @@ -537,6 +541,11 @@ export interface UsageLog { stream: boolean duration_ms: number first_token_ms: number | null + + // 图片生成字段 + image_count: number + image_size: string | null + created_at: string user?: User diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue index 1026f7bc..cd294126 100644 --- a/frontend/src/views/admin/GroupsView.vue +++ b/frontend/src/views/admin/GroupsView.vue @@ -398,6 +398,51 @@ + +
+ +

+ {{ t('admin.groups.imagePricing.description') }} +

+
+
+ + +
+
+ + +
+
+ + +
+
+
+