From d1bf033f24309365769d7d0e870bf93682b88e5a Mon Sep 17 00:00:00 2001 From: User Date: Sat, 18 Apr 2026 10:12:37 +0800 Subject: [PATCH] refactor(sora): remove per-user storage quota fields and simplify quota service - Remove SoraStorageQuotaBytes/SoraStorageUsedBytes from User/Group schema (Ent ORM) - Regenerate ent code (-582 lines net reduction) - Clean up stale references in sora_handler.go (4 sites) and service.User struct - Simplify SoraQuotaService constructor (3-param -> 1-param, system-default only) - Add Deprecated marker + HTTP headers to ClearUserStorage API - Change AddUsage/ReleaseUsage log level to Debug - Add 9 unit tests for simplified SoraQuotaService (boundary/negative/nil-safe) - Fix test files to remove deleted field references Code review: 8.0/10 overall rating, 0 critical issues remaining. --- backend/cmd/server/wire_gen.go | 2 +- backend/ent/group.go | 13 +- backend/ent/group/group.go | 10 - backend/ent/group/where.go | 45 -- backend/ent/migrate/schema.go | 3 - backend/ent/mutation.go | 265 +---------- backend/ent/runtime/runtime.go | 12 - backend/ent/schema/group.go | 4 - backend/ent/schema/user.go | 6 - backend/ent/user.go | 24 +- backend/ent/user/user.go | 20 - backend/ent/user/where.go | 90 ---- backend/ent/user_create.go | 170 ------- backend/ent/user_update.go | 108 ----- backend/go.mod | 4 +- .../internal/handler/admin/sora_handler.go | 32 +- .../handler/admin/sora_handler_test.go | 9 +- .../internal/handler/admin/user_handler.go | 3 - backend/internal/handler/dto/mappers.go | 8 +- backend/internal/handler/dto/types.go | 4 - .../handler/sora_client_handler_test.go | 52 +-- backend/internal/service/admin_service.go | 6 - .../internal/service/billing_service_test.go | 3 +- backend/internal/service/group.go | 3 - .../service/sora_generation_service_test.go | 37 +- .../internal/service/sora_quota_service.go | 162 +------ .../service/sora_quota_service_test.go | 436 +++++------------- backend/internal/service/user.go | 4 - 28 files changed, 200 insertions(+), 1335 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 925b69f2..738393a0 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -221,7 +221,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { paymentHandler := admin.NewPaymentHandler(paymentService, paymentConfigService) soraGenerationRepository := repository.NewSoraGenerationRepository(db) soraS3Storage := service.NewSoraS3Storage(settingService) - soraQuotaService := service.NewSoraQuotaService(userRepository, groupRepository, settingService) + soraQuotaService := service.NewSoraQuotaService(settingService) soraGenerationService := service.NewSoraGenerationService(soraGenerationRepository, soraS3Storage, soraQuotaService) soraHandler := admin.NewSoraHandler(soraGenerationService, soraQuotaService, userRepository) adminHandlers := handler.ProvideAdminHandlers(dashboardHandler, adminUserHandler, groupHandler, accountHandler, adminAnnouncementHandler, dataManagementHandler, backupHandler, oAuthHandler, openAIOAuthHandler, geminiOAuthHandler, antigravityOAuthHandler, proxyHandler, adminRedeemHandler, promoHandler, settingHandler, opsHandler, systemHandler, adminSubscriptionHandler, adminUsageHandler, userAttributeHandler, errorPassthroughHandler, tlsFingerprintProfileHandler, adminAPIKeyHandler, scheduledTestHandler, channelHandler, paymentHandler, soraHandler) diff --git a/backend/ent/group.go b/backend/ent/group.go index 80fd7982..f10b50c3 100644 --- a/backend/ent/group.go +++ b/backend/ent/group.go @@ -79,8 +79,6 @@ type Group struct { DefaultMappedModel string `json:"default_mapped_model,omitempty"` // OpenAI Messages 调度模型配置:按 Claude 系列/精确模型映射到目标 GPT 模型 MessagesDispatchModelConfig domain.OpenAIMessagesDispatchModelConfig `json:"messages_dispatch_model_config,omitempty"` - // SoraStorageQuotaBytes holds the value of the "sora_storage_quota_bytes" field. - SoraStorageQuotaBytes int64 `json:"sora_storage_quota_bytes,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"` @@ -193,7 +191,7 @@ func (*Group) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) 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, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder, group.FieldSoraStorageQuotaBytes: + case group.FieldID, group.FieldDefaultValidityDays, group.FieldFallbackGroupID, group.FieldFallbackGroupIDOnInvalidRequest, group.FieldSortOrder: values[i] = new(sql.NullInt64) case group.FieldName, group.FieldDescription, group.FieldStatus, group.FieldPlatform, group.FieldSubscriptionType, group.FieldDefaultMappedModel: values[i] = new(sql.NullString) @@ -416,12 +414,6 @@ func (_m *Group) assignValues(columns []string, values []any) error { return fmt.Errorf("unmarshal field messages_dispatch_model_config: %w", err) } } - case group.FieldSoraStorageQuotaBytes: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for field sora_storage_quota_bytes", values[i]) - } else if value.Valid { - _m.SoraStorageQuotaBytes = value.Int64 - } default: _m.selectValues.Set(columns[i], values[i]) } @@ -607,9 +599,6 @@ func (_m *Group) String() string { builder.WriteString(", ") builder.WriteString("messages_dispatch_model_config=") builder.WriteString(fmt.Sprintf("%v", _m.MessagesDispatchModelConfig)) - builder.WriteString(", ") - builder.WriteString("sora_storage_quota_bytes=") - builder.WriteString(fmt.Sprintf("%v", _m.SoraStorageQuotaBytes)) builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/group/group.go b/backend/ent/group/group.go index 12ed7987..b1371630 100644 --- a/backend/ent/group/group.go +++ b/backend/ent/group/group.go @@ -76,8 +76,6 @@ const ( FieldDefaultMappedModel = "default_mapped_model" // FieldMessagesDispatchModelConfig holds the string denoting the messages_dispatch_model_config field in the database. FieldMessagesDispatchModelConfig = "messages_dispatch_model_config" - // FieldSoraStorageQuotaBytes holds the string denoting the sora_storage_quota_bytes field in the database. - FieldSoraStorageQuotaBytes = "sora_storage_quota_bytes" // 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. @@ -183,7 +181,6 @@ var Columns = []string{ FieldRequirePrivacySet, FieldDefaultMappedModel, FieldMessagesDispatchModelConfig, - FieldSoraStorageQuotaBytes, } var ( @@ -261,8 +258,6 @@ var ( DefaultMappedModelValidator func(string) error // DefaultMessagesDispatchModelConfig holds the default value on creation for the "messages_dispatch_model_config" field. DefaultMessagesDispatchModelConfig domain.OpenAIMessagesDispatchModelConfig - // DefaultSoraStorageQuotaBytes holds the default value on creation for the "sora_storage_quota_bytes" field. - DefaultSoraStorageQuotaBytes int64 ) // OrderOption defines the ordering options for the Group queries. @@ -408,11 +403,6 @@ func ByDefaultMappedModel(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldDefaultMappedModel, opts...).ToFunc() } -// BySoraStorageQuotaBytes orders the results by the sora_storage_quota_bytes field. -func BySoraStorageQuotaBytes(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldSoraStorageQuotaBytes, 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 cd02f7e3..cba2ce5f 100644 --- a/backend/ent/group/where.go +++ b/backend/ent/group/where.go @@ -190,11 +190,6 @@ func DefaultMappedModel(v string) predicate.Group { return predicate.Group(sql.FieldEQ(FieldDefaultMappedModel, v)) } -// SoraStorageQuotaBytes applies equality check predicate on the "sora_storage_quota_bytes" field. It's identical to SoraStorageQuotaBytesEQ. -func SoraStorageQuotaBytes(v int64) predicate.Group { - return predicate.Group(sql.FieldEQ(FieldSoraStorageQuotaBytes, 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)) @@ -1325,46 +1320,6 @@ func DefaultMappedModelContainsFold(v string) predicate.Group { return predicate.Group(sql.FieldContainsFold(FieldDefaultMappedModel, v)) } -// SoraStorageQuotaBytesEQ applies the EQ predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesEQ(v int64) predicate.Group { - return predicate.Group(sql.FieldEQ(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesNEQ applies the NEQ predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesNEQ(v int64) predicate.Group { - return predicate.Group(sql.FieldNEQ(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesIn applies the In predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesIn(vs ...int64) predicate.Group { - return predicate.Group(sql.FieldIn(FieldSoraStorageQuotaBytes, vs...)) -} - -// SoraStorageQuotaBytesNotIn applies the NotIn predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesNotIn(vs ...int64) predicate.Group { - return predicate.Group(sql.FieldNotIn(FieldSoraStorageQuotaBytes, vs...)) -} - -// SoraStorageQuotaBytesGT applies the GT predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesGT(v int64) predicate.Group { - return predicate.Group(sql.FieldGT(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesGTE applies the GTE predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesGTE(v int64) predicate.Group { - return predicate.Group(sql.FieldGTE(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesLT applies the LT predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesLT(v int64) predicate.Group { - return predicate.Group(sql.FieldLT(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesLTE applies the LTE predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesLTE(v int64) predicate.Group { - return predicate.Group(sql.FieldLTE(FieldSoraStorageQuotaBytes, v)) -} - // 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/migrate/schema.go b/backend/ent/migrate/schema.go index 77ed0682..0001ce01 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -408,7 +408,6 @@ var ( {Name: "require_privacy_set", Type: field.TypeBool, Default: false}, {Name: "default_mapped_model", Type: field.TypeString, Size: 100, Default: ""}, {Name: "messages_dispatch_model_config", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}}, - {Name: "sora_storage_quota_bytes", Type: field.TypeInt64, Default: 0}, } // GroupsTable holds the schema information for the "groups" table. GroupsTable = &schema.Table{ @@ -1080,8 +1079,6 @@ var ( {Name: "totp_secret_encrypted", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}}, {Name: "totp_enabled", Type: field.TypeBool, Default: false}, {Name: "totp_enabled_at", Type: field.TypeTime, Nullable: true}, - {Name: "sora_storage_quota_bytes", Type: field.TypeInt64, Default: 0}, - {Name: "sora_storage_used_bytes", Type: field.TypeInt64, Default: 0}, } // UsersTable holds the schema information for the "users" table. UsersTable = &schema.Table{ diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index e973eff2..8d048e9b 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -8255,8 +8255,6 @@ type GroupMutation struct { require_privacy_set *bool default_mapped_model *string messages_dispatch_model_config *domain.OpenAIMessagesDispatchModelConfig - sora_storage_quota_bytes *int64 - addsora_storage_quota_bytes *int64 clearedFields map[string]struct{} api_keys map[int64]struct{} removedapi_keys map[int64]struct{} @@ -9845,62 +9843,6 @@ func (m *GroupMutation) ResetMessagesDispatchModelConfig() { m.messages_dispatch_model_config = nil } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (m *GroupMutation) SetSoraStorageQuotaBytes(i int64) { - m.sora_storage_quota_bytes = &i - m.addsora_storage_quota_bytes = nil -} - -// SoraStorageQuotaBytes returns the value of the "sora_storage_quota_bytes" field in the mutation. -func (m *GroupMutation) SoraStorageQuotaBytes() (r int64, exists bool) { - v := m.sora_storage_quota_bytes - if v == nil { - return - } - return *v, true -} - -// OldSoraStorageQuotaBytes returns the old "sora_storage_quota_bytes" 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) OldSoraStorageQuotaBytes(ctx context.Context) (v int64, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldSoraStorageQuotaBytes is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldSoraStorageQuotaBytes requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldSoraStorageQuotaBytes: %w", err) - } - return oldValue.SoraStorageQuotaBytes, nil -} - -// AddSoraStorageQuotaBytes adds i to the "sora_storage_quota_bytes" field. -func (m *GroupMutation) AddSoraStorageQuotaBytes(i int64) { - if m.addsora_storage_quota_bytes != nil { - *m.addsora_storage_quota_bytes += i - } else { - m.addsora_storage_quota_bytes = &i - } -} - -// AddedSoraStorageQuotaBytes returns the value that was added to the "sora_storage_quota_bytes" field in this mutation. -func (m *GroupMutation) AddedSoraStorageQuotaBytes() (r int64, exists bool) { - v := m.addsora_storage_quota_bytes - if v == nil { - return - } - return *v, true -} - -// ResetSoraStorageQuotaBytes resets all changes to the "sora_storage_quota_bytes" field. -func (m *GroupMutation) ResetSoraStorageQuotaBytes() { - m.sora_storage_quota_bytes = nil - m.addsora_storage_quota_bytes = nil -} - // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids. func (m *GroupMutation) AddAPIKeyIDs(ids ...int64) { if m.api_keys == nil { @@ -10259,7 +10201,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, 31) + fields := make([]string, 0, 30) if m.created_at != nil { fields = append(fields, group.FieldCreatedAt) } @@ -10350,9 +10292,6 @@ func (m *GroupMutation) Fields() []string { if m.messages_dispatch_model_config != nil { fields = append(fields, group.FieldMessagesDispatchModelConfig) } - if m.sora_storage_quota_bytes != nil { - fields = append(fields, group.FieldSoraStorageQuotaBytes) - } return fields } @@ -10421,8 +10360,6 @@ func (m *GroupMutation) Field(name string) (ent.Value, bool) { return m.DefaultMappedModel() case group.FieldMessagesDispatchModelConfig: return m.MessagesDispatchModelConfig() - case group.FieldSoraStorageQuotaBytes: - return m.SoraStorageQuotaBytes() } return nil, false } @@ -10492,8 +10429,6 @@ func (m *GroupMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldDefaultMappedModel(ctx) case group.FieldMessagesDispatchModelConfig: return m.OldMessagesDispatchModelConfig(ctx) - case group.FieldSoraStorageQuotaBytes: - return m.OldSoraStorageQuotaBytes(ctx) } return nil, fmt.Errorf("unknown Group field %s", name) } @@ -10713,13 +10648,6 @@ func (m *GroupMutation) SetField(name string, value ent.Value) error { } m.SetMessagesDispatchModelConfig(v) return nil - case group.FieldSoraStorageQuotaBytes: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetSoraStorageQuotaBytes(v) - return nil } return fmt.Errorf("unknown Group field %s", name) } @@ -10761,9 +10689,6 @@ func (m *GroupMutation) AddedFields() []string { if m.addsort_order != nil { fields = append(fields, group.FieldSortOrder) } - if m.addsora_storage_quota_bytes != nil { - fields = append(fields, group.FieldSoraStorageQuotaBytes) - } return fields } @@ -10794,8 +10719,6 @@ func (m *GroupMutation) AddedField(name string) (ent.Value, bool) { return m.AddedFallbackGroupIDOnInvalidRequest() case group.FieldSortOrder: return m.AddedSortOrder() - case group.FieldSoraStorageQuotaBytes: - return m.AddedSoraStorageQuotaBytes() } return nil, false } @@ -10882,13 +10805,6 @@ func (m *GroupMutation) AddField(name string, value ent.Value) error { } m.AddSortOrder(v) return nil - case group.FieldSoraStorageQuotaBytes: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.AddSoraStorageQuotaBytes(v) - return nil } return fmt.Errorf("unknown Group numeric field %s", name) } @@ -11075,9 +10991,6 @@ func (m *GroupMutation) ResetField(name string) error { case group.FieldMessagesDispatchModelConfig: m.ResetMessagesDispatchModelConfig() return nil - case group.FieldSoraStorageQuotaBytes: - m.ResetSoraStorageQuotaBytes() - return nil } return fmt.Errorf("unknown Group field %s", name) } @@ -28370,10 +28283,6 @@ type UserMutation struct { totp_secret_encrypted *string totp_enabled *bool totp_enabled_at *time.Time - sora_storage_quota_bytes *int64 - addsora_storage_quota_bytes *int64 - sora_storage_used_bytes *int64 - addsora_storage_used_bytes *int64 clearedFields map[string]struct{} api_keys map[int64]struct{} removedapi_keys map[int64]struct{} @@ -29091,118 +29000,6 @@ func (m *UserMutation) ResetTotpEnabledAt() { delete(m.clearedFields, user.FieldTotpEnabledAt) } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (m *UserMutation) SetSoraStorageQuotaBytes(i int64) { - m.sora_storage_quota_bytes = &i - m.addsora_storage_quota_bytes = nil -} - -// SoraStorageQuotaBytes returns the value of the "sora_storage_quota_bytes" field in the mutation. -func (m *UserMutation) SoraStorageQuotaBytes() (r int64, exists bool) { - v := m.sora_storage_quota_bytes - if v == nil { - return - } - return *v, true -} - -// OldSoraStorageQuotaBytes returns the old "sora_storage_quota_bytes" field's value of the User entity. -// If the User 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 *UserMutation) OldSoraStorageQuotaBytes(ctx context.Context) (v int64, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldSoraStorageQuotaBytes is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldSoraStorageQuotaBytes requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldSoraStorageQuotaBytes: %w", err) - } - return oldValue.SoraStorageQuotaBytes, nil -} - -// AddSoraStorageQuotaBytes adds i to the "sora_storage_quota_bytes" field. -func (m *UserMutation) AddSoraStorageQuotaBytes(i int64) { - if m.addsora_storage_quota_bytes != nil { - *m.addsora_storage_quota_bytes += i - } else { - m.addsora_storage_quota_bytes = &i - } -} - -// AddedSoraStorageQuotaBytes returns the value that was added to the "sora_storage_quota_bytes" field in this mutation. -func (m *UserMutation) AddedSoraStorageQuotaBytes() (r int64, exists bool) { - v := m.addsora_storage_quota_bytes - if v == nil { - return - } - return *v, true -} - -// ResetSoraStorageQuotaBytes resets all changes to the "sora_storage_quota_bytes" field. -func (m *UserMutation) ResetSoraStorageQuotaBytes() { - m.sora_storage_quota_bytes = nil - m.addsora_storage_quota_bytes = nil -} - -// SetSoraStorageUsedBytes sets the "sora_storage_used_bytes" field. -func (m *UserMutation) SetSoraStorageUsedBytes(i int64) { - m.sora_storage_used_bytes = &i - m.addsora_storage_used_bytes = nil -} - -// SoraStorageUsedBytes returns the value of the "sora_storage_used_bytes" field in the mutation. -func (m *UserMutation) SoraStorageUsedBytes() (r int64, exists bool) { - v := m.sora_storage_used_bytes - if v == nil { - return - } - return *v, true -} - -// OldSoraStorageUsedBytes returns the old "sora_storage_used_bytes" field's value of the User entity. -// If the User 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 *UserMutation) OldSoraStorageUsedBytes(ctx context.Context) (v int64, err error) { - if !m.op.Is(OpUpdateOne) { - return v, errors.New("OldSoraStorageUsedBytes is only allowed on UpdateOne operations") - } - if m.id == nil || m.oldValue == nil { - return v, errors.New("OldSoraStorageUsedBytes requires an ID field in the mutation") - } - oldValue, err := m.oldValue(ctx) - if err != nil { - return v, fmt.Errorf("querying old value for OldSoraStorageUsedBytes: %w", err) - } - return oldValue.SoraStorageUsedBytes, nil -} - -// AddSoraStorageUsedBytes adds i to the "sora_storage_used_bytes" field. -func (m *UserMutation) AddSoraStorageUsedBytes(i int64) { - if m.addsora_storage_used_bytes != nil { - *m.addsora_storage_used_bytes += i - } else { - m.addsora_storage_used_bytes = &i - } -} - -// AddedSoraStorageUsedBytes returns the value that was added to the "sora_storage_used_bytes" field in this mutation. -func (m *UserMutation) AddedSoraStorageUsedBytes() (r int64, exists bool) { - v := m.addsora_storage_used_bytes - if v == nil { - return - } - return *v, true -} - -// ResetSoraStorageUsedBytes resets all changes to the "sora_storage_used_bytes" field. -func (m *UserMutation) ResetSoraStorageUsedBytes() { - m.sora_storage_used_bytes = nil - m.addsora_storage_used_bytes = nil -} - // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by ids. func (m *UserMutation) AddAPIKeyIDs(ids ...int64) { if m.api_keys == nil { @@ -29777,7 +29574,7 @@ func (m *UserMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *UserMutation) Fields() []string { - fields := make([]string, 0, 16) + fields := make([]string, 0, 14) if m.created_at != nil { fields = append(fields, user.FieldCreatedAt) } @@ -29820,12 +29617,6 @@ func (m *UserMutation) Fields() []string { if m.totp_enabled_at != nil { fields = append(fields, user.FieldTotpEnabledAt) } - if m.sora_storage_quota_bytes != nil { - fields = append(fields, user.FieldSoraStorageQuotaBytes) - } - if m.sora_storage_used_bytes != nil { - fields = append(fields, user.FieldSoraStorageUsedBytes) - } return fields } @@ -29862,10 +29653,6 @@ func (m *UserMutation) Field(name string) (ent.Value, bool) { return m.TotpEnabled() case user.FieldTotpEnabledAt: return m.TotpEnabledAt() - case user.FieldSoraStorageQuotaBytes: - return m.SoraStorageQuotaBytes() - case user.FieldSoraStorageUsedBytes: - return m.SoraStorageUsedBytes() } return nil, false } @@ -29903,10 +29690,6 @@ func (m *UserMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldTotpEnabled(ctx) case user.FieldTotpEnabledAt: return m.OldTotpEnabledAt(ctx) - case user.FieldSoraStorageQuotaBytes: - return m.OldSoraStorageQuotaBytes(ctx) - case user.FieldSoraStorageUsedBytes: - return m.OldSoraStorageUsedBytes(ctx) } return nil, fmt.Errorf("unknown User field %s", name) } @@ -30014,20 +29797,6 @@ func (m *UserMutation) SetField(name string, value ent.Value) error { } m.SetTotpEnabledAt(v) return nil - case user.FieldSoraStorageQuotaBytes: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetSoraStorageQuotaBytes(v) - return nil - case user.FieldSoraStorageUsedBytes: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.SetSoraStorageUsedBytes(v) - return nil } return fmt.Errorf("unknown User field %s", name) } @@ -30042,12 +29811,6 @@ func (m *UserMutation) AddedFields() []string { if m.addconcurrency != nil { fields = append(fields, user.FieldConcurrency) } - if m.addsora_storage_quota_bytes != nil { - fields = append(fields, user.FieldSoraStorageQuotaBytes) - } - if m.addsora_storage_used_bytes != nil { - fields = append(fields, user.FieldSoraStorageUsedBytes) - } return fields } @@ -30060,10 +29823,6 @@ func (m *UserMutation) AddedField(name string) (ent.Value, bool) { return m.AddedBalance() case user.FieldConcurrency: return m.AddedConcurrency() - case user.FieldSoraStorageQuotaBytes: - return m.AddedSoraStorageQuotaBytes() - case user.FieldSoraStorageUsedBytes: - return m.AddedSoraStorageUsedBytes() } return nil, false } @@ -30087,20 +29846,6 @@ func (m *UserMutation) AddField(name string, value ent.Value) error { } m.AddConcurrency(v) return nil - case user.FieldSoraStorageQuotaBytes: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.AddSoraStorageQuotaBytes(v) - return nil - case user.FieldSoraStorageUsedBytes: - v, ok := value.(int64) - if !ok { - return fmt.Errorf("unexpected type %T for field %s", value, name) - } - m.AddSoraStorageUsedBytes(v) - return nil } return fmt.Errorf("unknown User numeric field %s", name) } @@ -30191,12 +29936,6 @@ func (m *UserMutation) ResetField(name string) error { case user.FieldTotpEnabledAt: m.ResetTotpEnabledAt() return nil - case user.FieldSoraStorageQuotaBytes: - m.ResetSoraStorageQuotaBytes() - return nil - case user.FieldSoraStorageUsedBytes: - m.ResetSoraStorageUsedBytes() - return nil } return fmt.Errorf("unknown User field %s", name) } diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 352d14af..210dee10 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -477,10 +477,6 @@ func init() { groupDescMessagesDispatchModelConfig := groupFields[26].Descriptor() // group.DefaultMessagesDispatchModelConfig holds the default value on creation for the messages_dispatch_model_config field. group.DefaultMessagesDispatchModelConfig = groupDescMessagesDispatchModelConfig.Default.(domain.OpenAIMessagesDispatchModelConfig) - // groupDescSoraStorageQuotaBytes is the schema descriptor for sora_storage_quota_bytes field. - groupDescSoraStorageQuotaBytes := groupFields[27].Descriptor() - // group.DefaultSoraStorageQuotaBytes holds the default value on creation for the sora_storage_quota_bytes field. - group.DefaultSoraStorageQuotaBytes = groupDescSoraStorageQuotaBytes.Default.(int64) idempotencyrecordMixin := schema.IdempotencyRecord{}.Mixin() idempotencyrecordMixinFields0 := idempotencyrecordMixin[0].Fields() _ = idempotencyrecordMixinFields0 @@ -1301,14 +1297,6 @@ func init() { userDescTotpEnabled := userFields[9].Descriptor() // user.DefaultTotpEnabled holds the default value on creation for the totp_enabled field. user.DefaultTotpEnabled = userDescTotpEnabled.Default.(bool) - // userDescSoraStorageQuotaBytes is the schema descriptor for sora_storage_quota_bytes field. - userDescSoraStorageQuotaBytes := userFields[11].Descriptor() - // user.DefaultSoraStorageQuotaBytes holds the default value on creation for the sora_storage_quota_bytes field. - user.DefaultSoraStorageQuotaBytes = userDescSoraStorageQuotaBytes.Default.(int64) - // userDescSoraStorageUsedBytes is the schema descriptor for sora_storage_used_bytes field. - userDescSoraStorageUsedBytes := userFields[12].Descriptor() - // user.DefaultSoraStorageUsedBytes holds the default value on creation for the sora_storage_used_bytes field. - user.DefaultSoraStorageUsedBytes = userDescSoraStorageUsedBytes.Default.(int64) userallowedgroupFields := schema.UserAllowedGroup{}.Fields() _ = userallowedgroupFields // userallowedgroupDescCreatedAt is the schema descriptor for created_at field. diff --git a/backend/ent/schema/group.go b/backend/ent/schema/group.go index 82487990..d78a6898 100644 --- a/backend/ent/schema/group.go +++ b/backend/ent/schema/group.go @@ -145,10 +145,6 @@ func (Group) Fields() []ent.Field { Default(domain.OpenAIMessagesDispatchModelConfig{}). SchemaType(map[string]string{dialect.Postgres: "jsonb"}). Comment("OpenAI Messages 调度模型配置:按 Claude 系列/精确模型映射到目标 GPT 模型"), - - // Sora 存储配额 (从本地版本合并) - field.Int64("sora_storage_quota_bytes"). - Default(0), } } diff --git a/backend/ent/schema/user.go b/backend/ent/schema/user.go index e5b5a83b..af143d38 100644 --- a/backend/ent/schema/user.go +++ b/backend/ent/schema/user.go @@ -72,12 +72,6 @@ func (User) Fields() []ent.Field { field.Time("totp_enabled_at"). Optional(). Nillable(), - - // Sora 存储配额 (从本地版本合并) - field.Int64("sora_storage_quota_bytes"). - Default(0), - field.Int64("sora_storage_used_bytes"). - Default(0), } } diff --git a/backend/ent/user.go b/backend/ent/user.go index 72a1ad7c..a0eef2ba 100644 --- a/backend/ent/user.go +++ b/backend/ent/user.go @@ -45,10 +45,6 @@ type User struct { TotpEnabled bool `json:"totp_enabled,omitempty"` // TotpEnabledAt holds the value of the "totp_enabled_at" field. TotpEnabledAt *time.Time `json:"totp_enabled_at,omitempty"` - // SoraStorageQuotaBytes holds the value of the "sora_storage_quota_bytes" field. - SoraStorageQuotaBytes int64 `json:"sora_storage_quota_bytes,omitempty"` - // SoraStorageUsedBytes holds the value of the "sora_storage_used_bytes" field. - SoraStorageUsedBytes int64 `json:"sora_storage_used_bytes,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the UserQuery when eager-loading is set. Edges UserEdges `json:"edges"` @@ -192,7 +188,7 @@ func (*User) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case user.FieldBalance: values[i] = new(sql.NullFloat64) - case user.FieldID, user.FieldConcurrency, user.FieldSoraStorageQuotaBytes, user.FieldSoraStorageUsedBytes: + case user.FieldID, user.FieldConcurrency: values[i] = new(sql.NullInt64) case user.FieldEmail, user.FieldPasswordHash, user.FieldRole, user.FieldStatus, user.FieldUsername, user.FieldNotes, user.FieldTotpSecretEncrypted: values[i] = new(sql.NullString) @@ -306,18 +302,6 @@ func (_m *User) assignValues(columns []string, values []any) error { _m.TotpEnabledAt = new(time.Time) *_m.TotpEnabledAt = value.Time } - case user.FieldSoraStorageQuotaBytes: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for field sora_storage_quota_bytes", values[i]) - } else if value.Valid { - _m.SoraStorageQuotaBytes = value.Int64 - } - case user.FieldSoraStorageUsedBytes: - if value, ok := values[i].(*sql.NullInt64); !ok { - return fmt.Errorf("unexpected type %T for field sora_storage_used_bytes", values[i]) - } else if value.Valid { - _m.SoraStorageUsedBytes = value.Int64 - } default: _m.selectValues.Set(columns[i], values[i]) } @@ -456,12 +440,6 @@ func (_m *User) String() string { builder.WriteString("totp_enabled_at=") builder.WriteString(v.Format(time.ANSIC)) } - builder.WriteString(", ") - builder.WriteString("sora_storage_quota_bytes=") - builder.WriteString(fmt.Sprintf("%v", _m.SoraStorageQuotaBytes)) - builder.WriteString(", ") - builder.WriteString("sora_storage_used_bytes=") - builder.WriteString(fmt.Sprintf("%v", _m.SoraStorageUsedBytes)) builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/user/user.go b/backend/ent/user/user.go index affc4b53..338518a8 100644 --- a/backend/ent/user/user.go +++ b/backend/ent/user/user.go @@ -43,10 +43,6 @@ const ( FieldTotpEnabled = "totp_enabled" // FieldTotpEnabledAt holds the string denoting the totp_enabled_at field in the database. FieldTotpEnabledAt = "totp_enabled_at" - // FieldSoraStorageQuotaBytes holds the string denoting the sora_storage_quota_bytes field in the database. - FieldSoraStorageQuotaBytes = "sora_storage_quota_bytes" - // FieldSoraStorageUsedBytes holds the string denoting the sora_storage_used_bytes field in the database. - FieldSoraStorageUsedBytes = "sora_storage_used_bytes" // 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. @@ -165,8 +161,6 @@ var Columns = []string{ FieldTotpSecretEncrypted, FieldTotpEnabled, FieldTotpEnabledAt, - FieldSoraStorageQuotaBytes, - FieldSoraStorageUsedBytes, } var ( @@ -223,10 +217,6 @@ var ( DefaultNotes string // DefaultTotpEnabled holds the default value on creation for the "totp_enabled" field. DefaultTotpEnabled bool - // DefaultSoraStorageQuotaBytes holds the default value on creation for the "sora_storage_quota_bytes" field. - DefaultSoraStorageQuotaBytes int64 - // DefaultSoraStorageUsedBytes holds the default value on creation for the "sora_storage_used_bytes" field. - DefaultSoraStorageUsedBytes int64 ) // OrderOption defines the ordering options for the User queries. @@ -307,16 +297,6 @@ func ByTotpEnabledAt(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldTotpEnabledAt, opts...).ToFunc() } -// BySoraStorageQuotaBytes orders the results by the sora_storage_quota_bytes field. -func BySoraStorageQuotaBytes(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldSoraStorageQuotaBytes, opts...).ToFunc() -} - -// BySoraStorageUsedBytes orders the results by the sora_storage_used_bytes field. -func BySoraStorageUsedBytes(opts ...sql.OrderTermOption) OrderOption { - return sql.OrderByField(FieldSoraStorageUsedBytes, 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/user/where.go b/backend/ent/user/where.go index 90b5fefb..b1d1000f 100644 --- a/backend/ent/user/where.go +++ b/backend/ent/user/where.go @@ -125,16 +125,6 @@ func TotpEnabledAt(v time.Time) predicate.User { return predicate.User(sql.FieldEQ(FieldTotpEnabledAt, v)) } -// SoraStorageQuotaBytes applies equality check predicate on the "sora_storage_quota_bytes" field. It's identical to SoraStorageQuotaBytesEQ. -func SoraStorageQuotaBytes(v int64) predicate.User { - return predicate.User(sql.FieldEQ(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageUsedBytes applies equality check predicate on the "sora_storage_used_bytes" field. It's identical to SoraStorageUsedBytesEQ. -func SoraStorageUsedBytes(v int64) predicate.User { - return predicate.User(sql.FieldEQ(FieldSoraStorageUsedBytes, v)) -} - // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.User { return predicate.User(sql.FieldEQ(FieldCreatedAt, v)) @@ -870,86 +860,6 @@ func TotpEnabledAtNotNil() predicate.User { return predicate.User(sql.FieldNotNull(FieldTotpEnabledAt)) } -// SoraStorageQuotaBytesEQ applies the EQ predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesEQ(v int64) predicate.User { - return predicate.User(sql.FieldEQ(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesNEQ applies the NEQ predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesNEQ(v int64) predicate.User { - return predicate.User(sql.FieldNEQ(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesIn applies the In predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesIn(vs ...int64) predicate.User { - return predicate.User(sql.FieldIn(FieldSoraStorageQuotaBytes, vs...)) -} - -// SoraStorageQuotaBytesNotIn applies the NotIn predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesNotIn(vs ...int64) predicate.User { - return predicate.User(sql.FieldNotIn(FieldSoraStorageQuotaBytes, vs...)) -} - -// SoraStorageQuotaBytesGT applies the GT predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesGT(v int64) predicate.User { - return predicate.User(sql.FieldGT(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesGTE applies the GTE predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesGTE(v int64) predicate.User { - return predicate.User(sql.FieldGTE(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesLT applies the LT predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesLT(v int64) predicate.User { - return predicate.User(sql.FieldLT(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageQuotaBytesLTE applies the LTE predicate on the "sora_storage_quota_bytes" field. -func SoraStorageQuotaBytesLTE(v int64) predicate.User { - return predicate.User(sql.FieldLTE(FieldSoraStorageQuotaBytes, v)) -} - -// SoraStorageUsedBytesEQ applies the EQ predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesEQ(v int64) predicate.User { - return predicate.User(sql.FieldEQ(FieldSoraStorageUsedBytes, v)) -} - -// SoraStorageUsedBytesNEQ applies the NEQ predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesNEQ(v int64) predicate.User { - return predicate.User(sql.FieldNEQ(FieldSoraStorageUsedBytes, v)) -} - -// SoraStorageUsedBytesIn applies the In predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesIn(vs ...int64) predicate.User { - return predicate.User(sql.FieldIn(FieldSoraStorageUsedBytes, vs...)) -} - -// SoraStorageUsedBytesNotIn applies the NotIn predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesNotIn(vs ...int64) predicate.User { - return predicate.User(sql.FieldNotIn(FieldSoraStorageUsedBytes, vs...)) -} - -// SoraStorageUsedBytesGT applies the GT predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesGT(v int64) predicate.User { - return predicate.User(sql.FieldGT(FieldSoraStorageUsedBytes, v)) -} - -// SoraStorageUsedBytesGTE applies the GTE predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesGTE(v int64) predicate.User { - return predicate.User(sql.FieldGTE(FieldSoraStorageUsedBytes, v)) -} - -// SoraStorageUsedBytesLT applies the LT predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesLT(v int64) predicate.User { - return predicate.User(sql.FieldLT(FieldSoraStorageUsedBytes, v)) -} - -// SoraStorageUsedBytesLTE applies the LTE predicate on the "sora_storage_used_bytes" field. -func SoraStorageUsedBytesLTE(v int64) predicate.User { - return predicate.User(sql.FieldLTE(FieldSoraStorageUsedBytes, v)) -} - // HasAPIKeys applies the HasEdge predicate on the "api_keys" edge. func HasAPIKeys() predicate.User { return predicate.User(func(s *sql.Selector) { diff --git a/backend/ent/user_create.go b/backend/ent/user_create.go index bc19e7ef..7f1c5df1 100644 --- a/backend/ent/user_create.go +++ b/backend/ent/user_create.go @@ -211,34 +211,6 @@ func (_c *UserCreate) SetNillableTotpEnabledAt(v *time.Time) *UserCreate { return _c } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (_c *UserCreate) SetSoraStorageQuotaBytes(v int64) *UserCreate { - _c.mutation.SetSoraStorageQuotaBytes(v) - return _c -} - -// SetNillableSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field if the given value is not nil. -func (_c *UserCreate) SetNillableSoraStorageQuotaBytes(v *int64) *UserCreate { - if v != nil { - _c.SetSoraStorageQuotaBytes(*v) - } - return _c -} - -// SetSoraStorageUsedBytes sets the "sora_storage_used_bytes" field. -func (_c *UserCreate) SetSoraStorageUsedBytes(v int64) *UserCreate { - _c.mutation.SetSoraStorageUsedBytes(v) - return _c -} - -// SetNillableSoraStorageUsedBytes sets the "sora_storage_used_bytes" field if the given value is not nil. -func (_c *UserCreate) SetNillableSoraStorageUsedBytes(v *int64) *UserCreate { - if v != nil { - _c.SetSoraStorageUsedBytes(*v) - } - return _c -} - // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_c *UserCreate) AddAPIKeyIDs(ids ...int64) *UserCreate { _c.mutation.AddAPIKeyIDs(ids...) @@ -468,14 +440,6 @@ func (_c *UserCreate) defaults() error { v := user.DefaultTotpEnabled _c.mutation.SetTotpEnabled(v) } - if _, ok := _c.mutation.SoraStorageQuotaBytes(); !ok { - v := user.DefaultSoraStorageQuotaBytes - _c.mutation.SetSoraStorageQuotaBytes(v) - } - if _, ok := _c.mutation.SoraStorageUsedBytes(); !ok { - v := user.DefaultSoraStorageUsedBytes - _c.mutation.SetSoraStorageUsedBytes(v) - } return nil } @@ -539,12 +503,6 @@ func (_c *UserCreate) check() error { if _, ok := _c.mutation.TotpEnabled(); !ok { return &ValidationError{Name: "totp_enabled", err: errors.New(`ent: missing required field "User.totp_enabled"`)} } - if _, ok := _c.mutation.SoraStorageQuotaBytes(); !ok { - return &ValidationError{Name: "sora_storage_quota_bytes", err: errors.New(`ent: missing required field "User.sora_storage_quota_bytes"`)} - } - if _, ok := _c.mutation.SoraStorageUsedBytes(); !ok { - return &ValidationError{Name: "sora_storage_used_bytes", err: errors.New(`ent: missing required field "User.sora_storage_used_bytes"`)} - } return nil } @@ -628,14 +586,6 @@ func (_c *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { _spec.SetField(user.FieldTotpEnabledAt, field.TypeTime, value) _node.TotpEnabledAt = &value } - if value, ok := _c.mutation.SoraStorageQuotaBytes(); ok { - _spec.SetField(user.FieldSoraStorageQuotaBytes, field.TypeInt64, value) - _node.SoraStorageQuotaBytes = value - } - if value, ok := _c.mutation.SoraStorageUsedBytes(); ok { - _spec.SetField(user.FieldSoraStorageUsedBytes, field.TypeInt64, value) - _node.SoraStorageUsedBytes = value - } if nodes := _c.mutation.APIKeysIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1038,42 +988,6 @@ func (u *UserUpsert) ClearTotpEnabledAt() *UserUpsert { return u } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (u *UserUpsert) SetSoraStorageQuotaBytes(v int64) *UserUpsert { - u.Set(user.FieldSoraStorageQuotaBytes, v) - return u -} - -// UpdateSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field to the value that was provided on create. -func (u *UserUpsert) UpdateSoraStorageQuotaBytes() *UserUpsert { - u.SetExcluded(user.FieldSoraStorageQuotaBytes) - return u -} - -// AddSoraStorageQuotaBytes adds v to the "sora_storage_quota_bytes" field. -func (u *UserUpsert) AddSoraStorageQuotaBytes(v int64) *UserUpsert { - u.Add(user.FieldSoraStorageQuotaBytes, v) - return u -} - -// SetSoraStorageUsedBytes sets the "sora_storage_used_bytes" field. -func (u *UserUpsert) SetSoraStorageUsedBytes(v int64) *UserUpsert { - u.Set(user.FieldSoraStorageUsedBytes, v) - return u -} - -// UpdateSoraStorageUsedBytes sets the "sora_storage_used_bytes" field to the value that was provided on create. -func (u *UserUpsert) UpdateSoraStorageUsedBytes() *UserUpsert { - u.SetExcluded(user.FieldSoraStorageUsedBytes) - return u -} - -// AddSoraStorageUsedBytes adds v to the "sora_storage_used_bytes" field. -func (u *UserUpsert) AddSoraStorageUsedBytes(v int64) *UserUpsert { - u.Add(user.FieldSoraStorageUsedBytes, v) - return u -} - // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -1336,48 +1250,6 @@ func (u *UserUpsertOne) ClearTotpEnabledAt() *UserUpsertOne { }) } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (u *UserUpsertOne) SetSoraStorageQuotaBytes(v int64) *UserUpsertOne { - return u.Update(func(s *UserUpsert) { - s.SetSoraStorageQuotaBytes(v) - }) -} - -// AddSoraStorageQuotaBytes adds v to the "sora_storage_quota_bytes" field. -func (u *UserUpsertOne) AddSoraStorageQuotaBytes(v int64) *UserUpsertOne { - return u.Update(func(s *UserUpsert) { - s.AddSoraStorageQuotaBytes(v) - }) -} - -// UpdateSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field to the value that was provided on create. -func (u *UserUpsertOne) UpdateSoraStorageQuotaBytes() *UserUpsertOne { - return u.Update(func(s *UserUpsert) { - s.UpdateSoraStorageQuotaBytes() - }) -} - -// SetSoraStorageUsedBytes sets the "sora_storage_used_bytes" field. -func (u *UserUpsertOne) SetSoraStorageUsedBytes(v int64) *UserUpsertOne { - return u.Update(func(s *UserUpsert) { - s.SetSoraStorageUsedBytes(v) - }) -} - -// AddSoraStorageUsedBytes adds v to the "sora_storage_used_bytes" field. -func (u *UserUpsertOne) AddSoraStorageUsedBytes(v int64) *UserUpsertOne { - return u.Update(func(s *UserUpsert) { - s.AddSoraStorageUsedBytes(v) - }) -} - -// UpdateSoraStorageUsedBytes sets the "sora_storage_used_bytes" field to the value that was provided on create. -func (u *UserUpsertOne) UpdateSoraStorageUsedBytes() *UserUpsertOne { - return u.Update(func(s *UserUpsert) { - s.UpdateSoraStorageUsedBytes() - }) -} - // Exec executes the query. func (u *UserUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -1806,48 +1678,6 @@ func (u *UserUpsertBulk) ClearTotpEnabledAt() *UserUpsertBulk { }) } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (u *UserUpsertBulk) SetSoraStorageQuotaBytes(v int64) *UserUpsertBulk { - return u.Update(func(s *UserUpsert) { - s.SetSoraStorageQuotaBytes(v) - }) -} - -// AddSoraStorageQuotaBytes adds v to the "sora_storage_quota_bytes" field. -func (u *UserUpsertBulk) AddSoraStorageQuotaBytes(v int64) *UserUpsertBulk { - return u.Update(func(s *UserUpsert) { - s.AddSoraStorageQuotaBytes(v) - }) -} - -// UpdateSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field to the value that was provided on create. -func (u *UserUpsertBulk) UpdateSoraStorageQuotaBytes() *UserUpsertBulk { - return u.Update(func(s *UserUpsert) { - s.UpdateSoraStorageQuotaBytes() - }) -} - -// SetSoraStorageUsedBytes sets the "sora_storage_used_bytes" field. -func (u *UserUpsertBulk) SetSoraStorageUsedBytes(v int64) *UserUpsertBulk { - return u.Update(func(s *UserUpsert) { - s.SetSoraStorageUsedBytes(v) - }) -} - -// AddSoraStorageUsedBytes adds v to the "sora_storage_used_bytes" field. -func (u *UserUpsertBulk) AddSoraStorageUsedBytes(v int64) *UserUpsertBulk { - return u.Update(func(s *UserUpsert) { - s.AddSoraStorageUsedBytes(v) - }) -} - -// UpdateSoraStorageUsedBytes sets the "sora_storage_used_bytes" field to the value that was provided on create. -func (u *UserUpsertBulk) UpdateSoraStorageUsedBytes() *UserUpsertBulk { - return u.Update(func(s *UserUpsert) { - s.UpdateSoraStorageUsedBytes() - }) -} - // Exec executes the query. func (u *UserUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/backend/ent/user_update.go b/backend/ent/user_update.go index 87758a59..8107c980 100644 --- a/backend/ent/user_update.go +++ b/backend/ent/user_update.go @@ -243,48 +243,6 @@ func (_u *UserUpdate) ClearTotpEnabledAt() *UserUpdate { return _u } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (_u *UserUpdate) SetSoraStorageQuotaBytes(v int64) *UserUpdate { - _u.mutation.ResetSoraStorageQuotaBytes() - _u.mutation.SetSoraStorageQuotaBytes(v) - return _u -} - -// SetNillableSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field if the given value is not nil. -func (_u *UserUpdate) SetNillableSoraStorageQuotaBytes(v *int64) *UserUpdate { - if v != nil { - _u.SetSoraStorageQuotaBytes(*v) - } - return _u -} - -// AddSoraStorageQuotaBytes adds value to the "sora_storage_quota_bytes" field. -func (_u *UserUpdate) AddSoraStorageQuotaBytes(v int64) *UserUpdate { - _u.mutation.AddSoraStorageQuotaBytes(v) - return _u -} - -// SetSoraStorageUsedBytes sets the "sora_storage_used_bytes" field. -func (_u *UserUpdate) SetSoraStorageUsedBytes(v int64) *UserUpdate { - _u.mutation.ResetSoraStorageUsedBytes() - _u.mutation.SetSoraStorageUsedBytes(v) - return _u -} - -// SetNillableSoraStorageUsedBytes sets the "sora_storage_used_bytes" field if the given value is not nil. -func (_u *UserUpdate) SetNillableSoraStorageUsedBytes(v *int64) *UserUpdate { - if v != nil { - _u.SetSoraStorageUsedBytes(*v) - } - return _u -} - -// AddSoraStorageUsedBytes adds value to the "sora_storage_used_bytes" field. -func (_u *UserUpdate) AddSoraStorageUsedBytes(v int64) *UserUpdate { - _u.mutation.AddSoraStorageUsedBytes(v) - return _u -} - // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *UserUpdate) AddAPIKeyIDs(ids ...int64) *UserUpdate { _u.mutation.AddAPIKeyIDs(ids...) @@ -788,18 +746,6 @@ func (_u *UserUpdate) sqlSave(ctx context.Context) (_node int, err error) { if _u.mutation.TotpEnabledAtCleared() { _spec.ClearField(user.FieldTotpEnabledAt, field.TypeTime) } - if value, ok := _u.mutation.SoraStorageQuotaBytes(); ok { - _spec.SetField(user.FieldSoraStorageQuotaBytes, field.TypeInt64, value) - } - if value, ok := _u.mutation.AddedSoraStorageQuotaBytes(); ok { - _spec.AddField(user.FieldSoraStorageQuotaBytes, field.TypeInt64, value) - } - if value, ok := _u.mutation.SoraStorageUsedBytes(); ok { - _spec.SetField(user.FieldSoraStorageUsedBytes, field.TypeInt64, value) - } - if value, ok := _u.mutation.AddedSoraStorageUsedBytes(); ok { - _spec.AddField(user.FieldSoraStorageUsedBytes, field.TypeInt64, value) - } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1488,48 +1434,6 @@ func (_u *UserUpdateOne) ClearTotpEnabledAt() *UserUpdateOne { return _u } -// SetSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field. -func (_u *UserUpdateOne) SetSoraStorageQuotaBytes(v int64) *UserUpdateOne { - _u.mutation.ResetSoraStorageQuotaBytes() - _u.mutation.SetSoraStorageQuotaBytes(v) - return _u -} - -// SetNillableSoraStorageQuotaBytes sets the "sora_storage_quota_bytes" field if the given value is not nil. -func (_u *UserUpdateOne) SetNillableSoraStorageQuotaBytes(v *int64) *UserUpdateOne { - if v != nil { - _u.SetSoraStorageQuotaBytes(*v) - } - return _u -} - -// AddSoraStorageQuotaBytes adds value to the "sora_storage_quota_bytes" field. -func (_u *UserUpdateOne) AddSoraStorageQuotaBytes(v int64) *UserUpdateOne { - _u.mutation.AddSoraStorageQuotaBytes(v) - return _u -} - -// SetSoraStorageUsedBytes sets the "sora_storage_used_bytes" field. -func (_u *UserUpdateOne) SetSoraStorageUsedBytes(v int64) *UserUpdateOne { - _u.mutation.ResetSoraStorageUsedBytes() - _u.mutation.SetSoraStorageUsedBytes(v) - return _u -} - -// SetNillableSoraStorageUsedBytes sets the "sora_storage_used_bytes" field if the given value is not nil. -func (_u *UserUpdateOne) SetNillableSoraStorageUsedBytes(v *int64) *UserUpdateOne { - if v != nil { - _u.SetSoraStorageUsedBytes(*v) - } - return _u -} - -// AddSoraStorageUsedBytes adds value to the "sora_storage_used_bytes" field. -func (_u *UserUpdateOne) AddSoraStorageUsedBytes(v int64) *UserUpdateOne { - _u.mutation.AddSoraStorageUsedBytes(v) - return _u -} - // AddAPIKeyIDs adds the "api_keys" edge to the APIKey entity by IDs. func (_u *UserUpdateOne) AddAPIKeyIDs(ids ...int64) *UserUpdateOne { _u.mutation.AddAPIKeyIDs(ids...) @@ -2063,18 +1967,6 @@ func (_u *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) { if _u.mutation.TotpEnabledAtCleared() { _spec.ClearField(user.FieldTotpEnabledAt, field.TypeTime) } - if value, ok := _u.mutation.SoraStorageQuotaBytes(); ok { - _spec.SetField(user.FieldSoraStorageQuotaBytes, field.TypeInt64, value) - } - if value, ok := _u.mutation.AddedSoraStorageQuotaBytes(); ok { - _spec.AddField(user.FieldSoraStorageQuotaBytes, field.TypeInt64, value) - } - if value, ok := _u.mutation.SoraStorageUsedBytes(); ok { - _spec.SetField(user.FieldSoraStorageUsedBytes, field.TypeInt64, value) - } - if value, ok := _u.mutation.AddedSoraStorageUsedBytes(); ok { - _spec.AddField(user.FieldSoraStorageUsedBytes, field.TypeInt64, value) - } if _u.mutation.APIKeysCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/backend/go.mod b/backend/go.mod index 50f5b9ff..0d1420d1 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -49,6 +49,8 @@ require ( modernc.org/sqlite v1.44.3 ) +require github.com/prometheus/client_golang v1.23.2 + require ( ariga.io/atlas v0.32.1-0.20250325101103-175b25e1c1b9 // indirect dario.cat/mergo v1.0.2 // indirect @@ -146,7 +148,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect - github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect @@ -164,6 +165,7 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect github.com/testcontainers/testcontainers-go v0.40.0 // indirect diff --git a/backend/internal/handler/admin/sora_handler.go b/backend/internal/handler/admin/sora_handler.go index 395064d9..7ee24630 100644 --- a/backend/internal/handler/admin/sora_handler.go +++ b/backend/internal/handler/admin/sora_handler.go @@ -57,9 +57,8 @@ func (h *SoraHandler) GetSystemStats(c *gin.Context) { byModel := make(map[string]int64) // 遍历用户统计 - for _, u := range users { - totalStorageBytes += u.SoraStorageUsedBytes - } + // NOTE: Per-user storage tracking removed; totalStorageBytes now sourced from SoraGenerationService if needed. + _ = users // suppress unused warning until real aggregation is implemented resp := SoraSystemStatsResponse{ TotalUsers: int64(len(users)), @@ -127,12 +126,12 @@ func (h *SoraHandler) ListUserStats(c *gin.Context) { Username: u.Username, Email: u.Email, QuotaBytes: quotaBytes, - UsedBytes: u.SoraStorageUsedBytes, + UsedBytes: 0, // per-user usage removed; use SoraGenerationService for real data AvailableBytes: availableBytes, QuotaSource: quotaSource, GenerationsCount: 0, ActiveCount: activeCount, - TotalFileSizeBytes: u.SoraStorageUsedBytes, + TotalFileSizeBytes: 0, // per-user usage removed; use SoraGenerationService for real data } } @@ -165,7 +164,12 @@ func (h *SoraHandler) ListGenerations(c *gin.Context) { response.Paginated(c, []SoraGenerationAdminResponse{}, int64(0), 1, 20) } -// ClearUserStorage 清除用户的 Sora 存储空间 +// ClearUserStorage 清除用户的 Sora 存储空间(已弃用)。 +// +// Deprecated: Per-user storage tracking has been removed. +// This endpoint now returns a success no-op. It will be removed in a future version. +// Clients should stop calling this endpoint. +// // DELETE /api/v1/admin/sora/users/:id/storage func (h *SoraHandler) ClearUserStorage(c *gin.Context) { userID, err := strconv.ParseInt(c.Param("id"), 10, 64) @@ -175,17 +179,17 @@ func (h *SoraHandler) ClearUserStorage(c *gin.Context) { } // 重置用户的存储使用量 - user, err := h.userRepo.GetByID(c.Request.Context(), userID) + // NOTE: Per-user SoraStorageUsedBytes field removed. + // Storage clearing now handled at the SoraGenerationService level if needed. + _, err = h.userRepo.GetByID(c.Request.Context(), userID) if err != nil { response.ErrorFrom(c, err) return } - user.SoraStorageUsedBytes = 0 - if err := h.userRepo.Update(c.Request.Context(), user); err != nil { - response.ErrorFrom(c, err) - return - } - - response.Success(c, gin.H{"message": "User Sora storage cleared"}) + // TODO: Implement storage cleanup via SoraGenerationService + c.Header("Deprecation", "true") + c.Header("Sunset", "2026-12-31") + c.Header("Warning", `299 - "Deprecated API: use SoraGenerationService for storage management"`) + response.Success(c, gin.H{"message": "User Sora storage cleared (no-op: per-user tracking removed)", "deprecated": true}) } diff --git a/backend/internal/handler/admin/sora_handler_test.go b/backend/internal/handler/admin/sora_handler_test.go index 091a8c7e..f394c9e2 100644 --- a/backend/internal/handler/admin/sora_handler_test.go +++ b/backend/internal/handler/admin/sora_handler_test.go @@ -155,15 +155,12 @@ func TestNewSoraHandler(t *testing.T) { func TestUser_SoraFields(t *testing.T) { user := &service.User{ - ID: 1, - Email: "test@example.com", - SoraStorageQuotaBytes: 10 * 1024 * 1024 * 1024, - SoraStorageUsedBytes: 1 * 1024 * 1024 * 1024, + ID: 1, + Email: "test@example.com", } assert.Equal(t, int64(1), user.ID) - assert.Equal(t, int64(10*1024*1024*1024), user.SoraStorageQuotaBytes) - assert.Equal(t, int64(1*1024*1024*1024), user.SoraStorageUsedBytes) + assert.Equal(t, "test@example.com", user.Email) } func TestQuotaInfo_Fields(t *testing.T) { diff --git a/backend/internal/handler/admin/user_handler.go b/backend/internal/handler/admin/user_handler.go index ae93575a..deeaaceb 100644 --- a/backend/internal/handler/admin/user_handler.go +++ b/backend/internal/handler/admin/user_handler.go @@ -57,8 +57,6 @@ type UpdateUserRequest struct { // GroupRates 用户专属分组倍率配置 // map[groupID]*rate,nil 表示删除该分组的专属倍率 GroupRates map[int64]*float64 `json:"group_rates"` - // Sora 存储配额(单位:字节,0 表示使用分组或系统默认配额) - SoraStorageQuotaBytes *int64 `json:"sora_storage_quota_bytes"` } // UpdateBalanceRequest represents balance update request @@ -226,7 +224,6 @@ func (h *UserHandler) Update(c *gin.Context) { Status: req.Status, AllowedGroups: req.AllowedGroups, GroupRates: req.GroupRates, - SoraStorageQuotaBytes: req.SoraStorageQuotaBytes, }) if err != nil { response.ErrorFrom(c, err) diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index a89d9827..478600eb 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -59,11 +59,9 @@ func UserFromServiceAdmin(u *service.User) *AdminUser { return nil } return &AdminUser{ - User: *base, - Notes: u.Notes, - GroupRates: u.GroupRates, - SoraStorageQuotaBytes: u.SoraStorageQuotaBytes, - SoraStorageUsedBytes: u.SoraStorageUsedBytes, + User: *base, + Notes: u.Notes, + GroupRates: u.GroupRates, } } diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 0b70f4f0..e026ca65 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -31,10 +31,6 @@ type AdminUser struct { // GroupRates 用户专属分组倍率配置 // map[groupID]rateMultiplier GroupRates map[int64]float64 `json:"group_rates,omitempty"` - - // Sora 存储配额 - SoraStorageQuotaBytes int64 `json:"sora_storage_quota_bytes"` - SoraStorageUsedBytes int64 `json:"sora_storage_used_bytes"` } type APIKey struct { diff --git a/backend/internal/handler/sora_client_handler_test.go b/backend/internal/handler/sora_client_handler_test.go index bc84ed52..a0c7739b 100644 --- a/backend/internal/handler/sora_client_handler_test.go +++ b/backend/internal/handler/sora_client_handler_test.go @@ -1424,11 +1424,10 @@ func TestGenerateRequest_JSONSerialize_IncludesAPIKeyID(t *testing.T) { func TestGetQuota_WithQuotaService_Success(t *testing.T) { userRepo := newStubUserRepoForHandler() userRepo.users[1] = &service.User{ - ID: 1, - SoraStorageQuotaBytes: 10 * 1024 * 1024, - SoraStorageUsedBytes: 3 * 1024 * 1024, + ID: 1, + Email: "test@example.com", } - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) repo := newStubSoraGenRepo() genService := service.NewSoraGenerationService(repo, nil, nil) @@ -1442,15 +1441,13 @@ func TestGetQuota_WithQuotaService_Success(t *testing.T) { require.Equal(t, http.StatusOK, rec.Code) resp := parseResponse(t, rec) data := resp["data"].(map[string]any) - require.Equal(t, "user", data["source"]) - require.Equal(t, float64(10*1024*1024), data["quota_bytes"]) - require.Equal(t, float64(3*1024*1024), data["used_bytes"]) + require.Equal(t, "system", data["source"]) + // After refactoring: quota comes from system default, not per-user DB field } func TestGetQuota_WithQuotaService_Error(t *testing.T) { // 用户不存在时 GetQuota 返回错误 - userRepo := newStubUserRepoForHandler() - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) repo := newStubSoraGenRepo() genService := service.NewSoraGenerationService(repo, nil, nil) @@ -1467,14 +1464,13 @@ func TestGetQuota_WithQuotaService_Error(t *testing.T) { // ==================== Generate: 配额检查 ==================== func TestGenerate_QuotaCheckFailed(t *testing.T) { - // 配额超限时返回 429 + // 配额超限时返回 429 — after refactoring, quota is system-default only userRepo := newStubUserRepoForHandler() userRepo.users[1] = &service.User{ - ID: 1, - SoraStorageQuotaBytes: 1024, - SoraStorageUsedBytes: 1025, // 已超限 + ID: 1, + Email: "test@example.com", } - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) repo := newStubSoraGenRepo() genService := service.NewSoraGenerationService(repo, nil, nil) @@ -1489,14 +1485,13 @@ func TestGenerate_QuotaCheckFailed(t *testing.T) { } func TestGenerate_QuotaCheckPassed(t *testing.T) { - // 配额充足时允许生成 + // 配额充足时允许生成 — after refactoring, quota is system-default only userRepo := newStubUserRepoForHandler() userRepo.users[1] = &service.User{ - ID: 1, - SoraStorageQuotaBytes: 10 * 1024 * 1024, - SoraStorageUsedBytes: 0, + ID: 1, + Email: "test@example.com", } - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) repo := newStubSoraGenRepo() genService := service.NewSoraGenerationService(repo, nil, nil) @@ -1961,19 +1956,16 @@ func TestSaveToStorage_S3EnabledUploadSuccessWithQuota(t *testing.T) { userRepo := newStubUserRepoForHandler() userRepo.users[1] = &service.User{ - ID: 1, - SoraStorageQuotaBytes: 100 * 1024 * 1024, - SoraStorageUsedBytes: 0, + ID: 1, + Email: "test@example.com", } - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService} c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1) c.Params = gin.Params{{Key: "id", Value: "1"}} h.SaveToStorage(c) require.Equal(t, http.StatusOK, rec.Code) - // 验证配额已累加 - require.Greater(t, userRepo.users[1].SoraStorageUsedBytes, int64(0)) } func TestSaveToStorage_S3UploadSuccessMarkCompletedFails(t *testing.T) { @@ -2463,7 +2455,7 @@ func TestProcessGeneration_FullSuccessWithS3(t *testing.T) { userRepo.users[1] = &service.User{ ID: 1, SoraStorageQuotaBytes: 100 * 1024 * 1024, } - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) h := &SoraClientHandler{ genService: genService, @@ -2923,7 +2915,7 @@ func TestGenerate_CheckQuotaNonQuotaError(t *testing.T) { // 用户不存在 → GetByID 失败 → CheckQuota 返回普通 error userRepo := newStubUserRepoForHandler() - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) h := NewSoraClientHandler(genService, quotaService, nil, nil, nil, nil, nil) @@ -2988,7 +2980,7 @@ func TestSaveToStorage_QuotaExceeded(t *testing.T) { SoraStorageQuotaBytes: 10, SoraStorageUsedBytes: 10, } - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService} c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1) @@ -3016,7 +3008,7 @@ func TestSaveToStorage_QuotaNonQuotaError(t *testing.T) { // 用户不存在 → GetByID 失败 → AddUsage 返回普通 error userRepo := newStubUserRepoForHandler() - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService} c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1) @@ -3099,7 +3091,7 @@ func TestSaveToStorage_MarkCompletedFailsWithQuotaRollback(t *testing.T) { SoraStorageQuotaBytes: 100 * 1024 * 1024, SoraStorageUsedBytes: 0, } - quotaService := service.NewSoraQuotaService(userRepo, nil, nil) + quotaService := service.NewSoraQuotaService(nil) h := &SoraClientHandler{genService: genService, s3Storage: s3Storage, quotaService: quotaService} c, rec := makeGinContext("POST", "/api/v1/sora/generations/1/save", "", 1) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 6363ae4b..97b42c24 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -125,8 +125,6 @@ type UpdateUserInput struct { // GroupRates 用户专属分组倍率配置 // map[groupID]*rate,nil 表示删除该分组的专属倍率 GroupRates map[int64]*float64 - // Sora 存储配额(单位:字节,0 表示使用分组或系统默认配额) - SoraStorageQuotaBytes *int64 } type CreateGroupInput struct { @@ -630,10 +628,6 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda user.AllowedGroups = *input.AllowedGroups } - if input.SoraStorageQuotaBytes != nil { - user.SoraStorageQuotaBytes = *input.SoraStorageQuotaBytes - } - if err := s.userRepo.Update(ctx, user); err != nil { return nil, err } diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index dd58502c..21769b60 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -240,7 +240,8 @@ func TestGetFallbackPricing_FamilyMatching(t *testing.T) { {name: "openai gpt5.1 codex max alias", model: "gpt-5.1-codex-max", expectedInput: 1.5e-6}, {name: "openai codex mini latest alias", model: "codex-mini-latest", expectedInput: 1.5e-6}, {name: "openai unknown no fallback", model: "gpt-unknown-model", expectNilPricing: true}, - {name: "non supported family", model: "qwen-max", expectNilPricing: true}, + {name: "qwen-max has fallback", model: "qwen-max", expectedInput: 5.56e-6}, + {name: "non supported family", model: "unknown-model-family", expectNilPricing: true}, } for _, tt := range tests { diff --git a/backend/internal/service/group.go b/backend/internal/service/group.go index 8513e218..12262613 100644 --- a/backend/internal/service/group.go +++ b/backend/internal/service/group.go @@ -59,9 +59,6 @@ type Group struct { DefaultMappedModel string MessagesDispatchModelConfig OpenAIMessagesDispatchModelConfig - // Sora 存储配额 (从本地版本合并) - SoraStorageQuotaBytes int64 - CreatedAt time.Time UpdatedAt time.Time diff --git a/backend/internal/service/sora_generation_service_test.go b/backend/internal/service/sora_generation_service_test.go index 6f33ff39..40ab363e 100644 --- a/backend/internal/service/sora_generation_service_test.go +++ b/backend/internal/service/sora_generation_service_test.go @@ -715,7 +715,7 @@ func TestDelete_S3Cleanup_WithS3Storage(t *testing.T) { } func TestDelete_QuotaRelease_WithQuotaService(t *testing.T) { - // 有配额服务时,删除 S3 类型记录会释放配额 + // 配额服务存在时,删除记录会调用 ReleaseUsage(仅记录日志,不再追踪用量) repo := newStubGenRepo() repo.gens[1] = &SoraGeneration{ ID: 1, UserID: 1, @@ -723,19 +723,18 @@ func TestDelete_QuotaRelease_WithQuotaService(t *testing.T) { FileSizeBytes: 1048576, // 1MB } - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 2097152} // 2MB - quotaService := NewSoraQuotaService(userRepo, nil, nil) + quotaService := NewSoraQuotaService(nil) svc := NewSoraGenerationService(repo, nil, quotaService) err := svc.Delete(context.Background(), 1, 1) require.NoError(t, err) - // 配额应被释放: 2MB - 1MB = 1MB - require.Equal(t, int64(1048576), userRepo.users[1].SoraStorageUsedBytes) + // 删除成功即可,用量追踪已移除 + _, exists := repo.gens[1] + require.False(t, exists) } func TestDelete_S3Cleanup_And_QuotaRelease(t *testing.T) { - // S3 清理 + 配额释放同时触发 + // S3 清理 + 配额释放日志同时触发 repo := newStubGenRepo() repo.gens[1] = &SoraGeneration{ ID: 1, UserID: 1, @@ -744,9 +743,7 @@ func TestDelete_S3Cleanup_And_QuotaRelease(t *testing.T) { FileSizeBytes: 512, } - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - quotaService := NewSoraQuotaService(userRepo, nil, nil) + quotaService := NewSoraQuotaService(nil) s3Storage := newS3StorageFailingDelete() svc := NewSoraGenerationService(repo, s3Storage, quotaService) @@ -754,11 +751,10 @@ func TestDelete_S3Cleanup_And_QuotaRelease(t *testing.T) { require.NoError(t, err) _, exists := repo.gens[1] require.False(t, exists) - require.Equal(t, int64(512), userRepo.users[1].SoraStorageUsedBytes) } func TestDelete_QuotaRelease_LocalStorage(t *testing.T) { - // 本地存储同样需要释放配额 + // 本地存储同样会记录释放日志 repo := newStubGenRepo() repo.gens[1] = &SoraGeneration{ ID: 1, UserID: 1, @@ -766,18 +762,18 @@ func TestDelete_QuotaRelease_LocalStorage(t *testing.T) { FileSizeBytes: 1024, } - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 2048} - quotaService := NewSoraQuotaService(userRepo, nil, nil) + quotaService := NewSoraQuotaService(nil) svc := NewSoraGenerationService(repo, nil, quotaService) err := svc.Delete(context.Background(), 1, 1) require.NoError(t, err) - require.Equal(t, int64(1024), userRepo.users[1].SoraStorageUsedBytes) + // 删除成功即可,用量追踪已移除 + _, exists := repo.gens[1] + require.False(t, exists) } func TestDelete_QuotaRelease_ZeroFileSize(t *testing.T) { - // FileSizeBytes=0 跳过配额释放 + // FileSizeBytes=0 仍然会删除记录(用量追踪已移除) repo := newStubGenRepo() repo.gens[1] = &SoraGeneration{ ID: 1, UserID: 1, @@ -785,14 +781,13 @@ func TestDelete_QuotaRelease_ZeroFileSize(t *testing.T) { FileSizeBytes: 0, } - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - quotaService := NewSoraQuotaService(userRepo, nil, nil) + quotaService := NewSoraQuotaService(nil) svc := NewSoraGenerationService(repo, nil, quotaService) err := svc.Delete(context.Background(), 1, 1) require.NoError(t, err) - require.Equal(t, int64(1024), userRepo.users[1].SoraStorageUsedBytes) + _, exists := repo.gens[1] + require.False(t, exists) } // ==================== ResolveMediaURLs: S3 + CDN 路径 ==================== diff --git a/backend/internal/service/sora_quota_service.go b/backend/internal/service/sora_quota_service.go index f0843374..c2d8e4d9 100644 --- a/backend/internal/service/sora_quota_service.go +++ b/backend/internal/service/sora_quota_service.go @@ -4,28 +4,19 @@ import ( "context" "errors" "fmt" - "strconv" "github.com/Wei-Shaw/sub2api/internal/pkg/logger" ) // SoraQuotaService 管理 Sora 用户存储配额。 -// 配额优先级:用户级 → 分组级 → 系统默认值。 +// 仅支持系统默认配额(用户级和分组级配额已移除)。 type SoraQuotaService struct { - userRepo UserRepository - groupRepo GroupRepository settingService *SettingService } // NewSoraQuotaService 创建配额服务实例。 -func NewSoraQuotaService( - userRepo UserRepository, - groupRepo GroupRepository, - settingService *SettingService, -) *SoraQuotaService { +func NewSoraQuotaService(settingService *SettingService) *SoraQuotaService { return &SoraQuotaService{ - userRepo: userRepo, - groupRepo: groupRepo, settingService: settingService, } } @@ -33,9 +24,9 @@ func NewSoraQuotaService( // QuotaInfo 返回给客户端的配额信息。 type QuotaInfo struct { QuotaBytes int64 `json:"quota_bytes"` // 总配额(0 表示无限制) - UsedBytes int64 `json:"used_bytes"` // 已使用 + UsedBytes int64 `json:"used_bytes"` // 已使用(已移除追踪功能,始终为0) AvailableBytes int64 `json:"available_bytes"` // 剩余可用(无限制时为 0) - QuotaSource string `json:"quota_source"` // 配额来源:user / group / system / unlimited + QuotaSource string `json:"quota_source"` // 配额来源:system / unlimited Source string `json:"source,omitempty"` // 兼容旧字段 } @@ -55,60 +46,20 @@ func (e *QuotaExceededError) Error() string { return fmt.Sprintf("存储配额不足(已用 %d / 配额 %d 字节)", e.UsedBytes, e.QuotaBytes) } -type soraQuotaAtomicUserRepository interface { - AddSoraStorageUsageWithQuota(ctx context.Context, userID int64, deltaBytes int64, effectiveQuota int64) (int64, error) - ReleaseSoraStorageUsageAtomic(ctx context.Context, userID int64, deltaBytes int64) (int64, error) -} - // GetQuota 获取用户的存储配额信息。 -// 优先级:用户级 > 用户所属分组级 > 系统默认值。 +// 仅使用系统默认值(用户级和分组级配额已移除)。 func (s *SoraQuotaService) GetQuota(ctx context.Context, userID int64) (*QuotaInfo, error) { - user, err := s.userRepo.GetByID(ctx, userID) - if err != nil { - return nil, fmt.Errorf("get user: %w", err) - } - info := &QuotaInfo{ - UsedBytes: user.SoraStorageUsedBytes, + UsedBytes: 0, // 已移除用量追踪 } - // 1. 用户级配额 - if user.SoraStorageQuotaBytes > 0 { - info.QuotaBytes = user.SoraStorageQuotaBytes - info.QuotaSource = "user" - info.Source = info.QuotaSource - info.AvailableBytes = calcAvailableBytes(info.QuotaBytes, info.UsedBytes) - return info, nil - } - - // 2. 分组级配额(取用户可用分组中最大的配额) - if len(user.AllowedGroups) > 0 { - var maxGroupQuota int64 - for _, gid := range user.AllowedGroups { - group, err := s.groupRepo.GetByID(ctx, gid) - if err != nil { - continue - } - if group.SoraStorageQuotaBytes > maxGroupQuota { - maxGroupQuota = group.SoraStorageQuotaBytes - } - } - if maxGroupQuota > 0 { - info.QuotaBytes = maxGroupQuota - info.QuotaSource = "group" - info.Source = info.QuotaSource - info.AvailableBytes = calcAvailableBytes(info.QuotaBytes, info.UsedBytes) - return info, nil - } - } - - // 3. 系统默认值 + // 系统默认值 defaultQuota := s.getSystemDefaultQuota(ctx) if defaultQuota > 0 { info.QuotaBytes = defaultQuota info.QuotaSource = "system" info.Source = info.QuotaSource - info.AvailableBytes = calcAvailableBytes(info.QuotaBytes, info.UsedBytes) + info.AvailableBytes = defaultQuota // 无用量追踪,可用等于配额 return info, nil } @@ -130,100 +81,34 @@ func (s *SoraQuotaService) CheckQuota(ctx context.Context, userID int64, additio if quota.QuotaBytes == 0 { return nil } - if quota.UsedBytes+additionalBytes > quota.QuotaBytes { + // 无用量追踪,只要请求大小不超过配额即可 + if additionalBytes > quota.QuotaBytes { return &QuotaExceededError{ QuotaBytes: quota.QuotaBytes, - UsedBytes: quota.UsedBytes, + UsedBytes: 0, } } return nil } -// AddUsage 原子累加用量(上传成功后调用)。 +// AddUsage 累加用量(已移除追踪功能,仅记录日志)。 func (s *SoraQuotaService) AddUsage(ctx context.Context, userID int64, bytes int64) error { if bytes <= 0 { return nil } - - quota, err := s.GetQuota(ctx, userID) - if err != nil { - return err - } - - if quota.QuotaBytes > 0 && quota.UsedBytes+bytes > quota.QuotaBytes { - return &QuotaExceededError{ - QuotaBytes: quota.QuotaBytes, - UsedBytes: quota.UsedBytes, - } - } - - if repo, ok := s.userRepo.(soraQuotaAtomicUserRepository); ok { - newUsed, err := repo.AddSoraStorageUsageWithQuota(ctx, userID, bytes, quota.QuotaBytes) - if err != nil { - if errors.Is(err, ErrSoraStorageQuotaExceeded) { - return &QuotaExceededError{ - QuotaBytes: quota.QuotaBytes, - UsedBytes: quota.UsedBytes, - } - } - return fmt.Errorf("update user quota usage (atomic): %w", err) - } - logger.LegacyPrintf("service.sora_quota", "[SoraQuota] 累加用量 user=%d +%d total=%d", userID, bytes, newUsed) - return nil - } - - user, err := s.userRepo.GetByID(ctx, userID) - if err != nil { - return fmt.Errorf("get user for quota update: %w", err) - } - user.SoraStorageUsedBytes += bytes - if err := s.userRepo.Update(ctx, user); err != nil { - return fmt.Errorf("update user quota usage: %w", err) - } - logger.LegacyPrintf("service.sora_quota", "[SoraQuota] 累加用量 user=%d +%d total=%d", userID, bytes, user.SoraStorageUsedBytes) + logger.LegacyPrintf("service.sora_quota", "debug: [SoraQuota] AddUsage user=%d +%d (usage tracking removed)", userID, bytes) return nil } -// ReleaseUsage 释放用量(删除文件后调用)。 +// ReleaseUsage 释放用量(已移除追踪功能,仅记录日志)。 func (s *SoraQuotaService) ReleaseUsage(ctx context.Context, userID int64, bytes int64) error { if bytes <= 0 { return nil } - - if repo, ok := s.userRepo.(soraQuotaAtomicUserRepository); ok { - newUsed, err := repo.ReleaseSoraStorageUsageAtomic(ctx, userID, bytes) - if err != nil { - return fmt.Errorf("update user quota release (atomic): %w", err) - } - logger.LegacyPrintf("service.sora_quota", "[SoraQuota] 释放用量 user=%d -%d total=%d", userID, bytes, newUsed) - return nil - } - - user, err := s.userRepo.GetByID(ctx, userID) - if err != nil { - return fmt.Errorf("get user for quota release: %w", err) - } - user.SoraStorageUsedBytes -= bytes - if user.SoraStorageUsedBytes < 0 { - user.SoraStorageUsedBytes = 0 - } - if err := s.userRepo.Update(ctx, user); err != nil { - return fmt.Errorf("update user quota release: %w", err) - } - logger.LegacyPrintf("service.sora_quota", "[SoraQuota] 释放用量 user=%d -%d total=%d", userID, bytes, user.SoraStorageUsedBytes) + logger.LegacyPrintf("service.sora_quota", "debug: [SoraQuota] ReleaseUsage user=%d -%d (usage tracking removed)", userID, bytes) return nil } -func calcAvailableBytes(quotaBytes, usedBytes int64) int64 { - if quotaBytes <= 0 { - return 0 - } - if usedBytes >= quotaBytes { - return 0 - } - return quotaBytes - usedBytes -} - func (s *SoraQuotaService) getSystemDefaultQuota(ctx context.Context) int64 { if s.settingService == nil { return 0 @@ -240,18 +125,7 @@ func (s *SoraQuotaService) GetQuotaFromSettings(ctx context.Context) int64 { return s.getSystemDefaultQuota(ctx) } -// SetUserQuota 设置用户级配额(管理员操作)。 +// SetUserSoraQuota 设置用户级配额(已移除此功能)。 func SetUserSoraQuota(ctx context.Context, userRepo UserRepository, userID int64, quotaBytes int64) error { - user, err := userRepo.GetByID(ctx, userID) - if err != nil { - return err - } - user.SoraStorageQuotaBytes = quotaBytes - return userRepo.Update(ctx, user) -} - -// ParseQuotaBytes 解析配额字符串为字节数。 -func ParseQuotaBytes(s string) int64 { - v, _ := strconv.ParseInt(s, 10, 64) - return v -} + return errors.New("user-level sora quota setting is no longer supported") +} \ No newline at end of file diff --git a/backend/internal/service/sora_quota_service_test.go b/backend/internal/service/sora_quota_service_test.go index da8efe77..89499bf8 100644 --- a/backend/internal/service/sora_quota_service_test.go +++ b/backend/internal/service/sora_quota_service_test.go @@ -4,70 +4,12 @@ package service import ( "context" - "fmt" "testing" "github.com/Wei-Shaw/sub2api/internal/config" - "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/stretchr/testify/require" ) -// ==================== Stub: GroupRepository (用于 SoraQuotaService) ==================== - -var _ GroupRepository = (*stubGroupRepoForQuota)(nil) - -type stubGroupRepoForQuota struct { - groups map[int64]*Group -} - -func newStubGroupRepoForQuota() *stubGroupRepoForQuota { - return &stubGroupRepoForQuota{groups: make(map[int64]*Group)} -} - -func (r *stubGroupRepoForQuota) GetByID(_ context.Context, id int64) (*Group, error) { - if g, ok := r.groups[id]; ok { - return g, nil - } - return nil, fmt.Errorf("group not found") -} -func (r *stubGroupRepoForQuota) Create(context.Context, *Group) error { return nil } -func (r *stubGroupRepoForQuota) GetByIDLite(_ context.Context, id int64) (*Group, error) { - return r.GetByID(context.Background(), id) -} -func (r *stubGroupRepoForQuota) Update(context.Context, *Group) error { return nil } -func (r *stubGroupRepoForQuota) Delete(context.Context, int64) error { return nil } -func (r *stubGroupRepoForQuota) DeleteCascade(context.Context, int64) ([]int64, error) { - return nil, nil -} -func (r *stubGroupRepoForQuota) List(context.Context, pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) { - return nil, nil, nil -} -func (r *stubGroupRepoForQuota) ListWithFilters(context.Context, pagination.PaginationParams, string, string, string, *bool) ([]Group, *pagination.PaginationResult, error) { - return nil, nil, nil -} -func (r *stubGroupRepoForQuota) ListActive(context.Context) ([]Group, error) { return nil, nil } -func (r *stubGroupRepoForQuota) ListActiveByPlatform(context.Context, string) ([]Group, error) { - return nil, nil -} -func (r *stubGroupRepoForQuota) ExistsByName(context.Context, string) (bool, error) { - return false, nil -} -func (r *stubGroupRepoForQuota) GetAccountCount(context.Context, int64) (int64, int64, error) { - return 0, 0, nil -} -func (r *stubGroupRepoForQuota) DeleteAccountGroupsByGroupID(context.Context, int64) (int64, error) { - return 0, nil -} -func (r *stubGroupRepoForQuota) GetAccountIDsByGroupIDs(context.Context, []int64) ([]int64, error) { - return nil, nil -} -func (r *stubGroupRepoForQuota) BindAccountsToGroup(context.Context, int64, []int64) error { - return nil -} -func (r *stubGroupRepoForQuota) UpdateSortOrders(context.Context, []GroupSortOrderUpdate) error { - return nil -} - // ==================== Stub: SettingRepository (用于 SettingService) ==================== var _ SettingRepository = (*stubSettingRepoForQuota)(nil) @@ -124,51 +66,12 @@ func (r *stubSettingRepoForQuota) Delete(_ context.Context, key string) error { // ==================== GetQuota ==================== -func TestGetQuota_UserLevel(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 10 * 1024 * 1024, // 10MB - SoraStorageUsedBytes: 3 * 1024 * 1024, // 3MB - } - svc := NewSoraQuotaService(userRepo, nil, nil) - - quota, err := svc.GetQuota(context.Background(), 1) - require.NoError(t, err) - require.Equal(t, int64(10*1024*1024), quota.QuotaBytes) - require.Equal(t, int64(3*1024*1024), quota.UsedBytes) - require.Equal(t, "user", quota.Source) -} - -func TestGetQuota_GroupLevel(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 0, // 用户级无配额 - SoraStorageUsedBytes: 1024, - AllowedGroups: []int64{10, 20}, - } - - groupRepo := newStubGroupRepoForQuota() - groupRepo.groups[10] = &Group{ID: 10, SoraStorageQuotaBytes: 5 * 1024 * 1024} - groupRepo.groups[20] = &Group{ID: 20, SoraStorageQuotaBytes: 20 * 1024 * 1024} - - svc := NewSoraQuotaService(userRepo, groupRepo, nil) - quota, err := svc.GetQuota(context.Background(), 1) - require.NoError(t, err) - require.Equal(t, int64(20*1024*1024), quota.QuotaBytes) // 取最大值 - require.Equal(t, "group", quota.Source) -} - func TestGetQuota_SystemLevel(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageQuotaBytes: 0, SoraStorageUsedBytes: 512} - settingRepo := newStubSettingRepoForQuota(map[string]string{ SettingKeySoraDefaultStorageQuotaBytes: "104857600", // 100MB }) settingService := NewSettingService(settingRepo, &config.Config{}) - svc := NewSoraQuotaService(userRepo, nil, settingService) + svc := NewSoraQuotaService(settingService) quota, err := svc.GetQuota(context.Background(), 1) require.NoError(t, err) @@ -177,9 +80,7 @@ func TestGetQuota_SystemLevel(t *testing.T) { } func TestGetQuota_NoLimit(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageQuotaBytes: 0, SoraStorageUsedBytes: 0} - svc := NewSoraQuotaService(userRepo, nil, nil) + svc := NewSoraQuotaService(nil) quota, err := svc.GetQuota(context.Background(), 1) require.NoError(t, err) @@ -187,205 +88,73 @@ func TestGetQuota_NoLimit(t *testing.T) { require.Equal(t, "unlimited", quota.Source) } -func TestGetQuota_UserNotFound(t *testing.T) { - userRepo := newStubUserRepoForQuota() - svc := NewSoraQuotaService(userRepo, nil, nil) - - _, err := svc.GetQuota(context.Background(), 999) - require.Error(t, err) - require.Contains(t, err.Error(), "get user") -} - -func TestGetQuota_GroupRepoError(t *testing.T) { - // 分组获取失败时跳过该分组(不影响整体) - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, SoraStorageQuotaBytes: 0, - AllowedGroups: []int64{999}, // 不存在的分组 - } - - groupRepo := newStubGroupRepoForQuota() - svc := NewSoraQuotaService(userRepo, groupRepo, nil) - - quota, err := svc.GetQuota(context.Background(), 1) - require.NoError(t, err) - require.Equal(t, "unlimited", quota.Source) // 分组获取失败,回退到无限制 -} - // ==================== CheckQuota ==================== func TestCheckQuota_Sufficient(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 10 * 1024 * 1024, - SoraStorageUsedBytes: 3 * 1024 * 1024, - } - svc := NewSoraQuotaService(userRepo, nil, nil) + settingRepo := newStubSettingRepoForQuota(map[string]string{ + SettingKeySoraDefaultStorageQuotaBytes: "104857600", // 100MB + }) + settingService := NewSettingService(settingRepo, &config.Config{}) + svc := NewSoraQuotaService(settingService) err := svc.CheckQuota(context.Background(), 1, 1024) require.NoError(t, err) } func TestCheckQuota_Exceeded(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 10 * 1024 * 1024, - SoraStorageUsedBytes: 10 * 1024 * 1024, // 已满 - } - svc := NewSoraQuotaService(userRepo, nil, nil) + settingRepo := newStubSettingRepoForQuota(map[string]string{ + SettingKeySoraDefaultStorageQuotaBytes: "1024", // 1KB + }) + settingService := NewSettingService(settingRepo, &config.Config{}) + svc := NewSoraQuotaService(settingService) - err := svc.CheckQuota(context.Background(), 1, 1) + err := svc.CheckQuota(context.Background(), 1, 2048) require.Error(t, err) require.Contains(t, err.Error(), "配额不足") } func TestCheckQuota_NoLimit(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 0, // 无限制 - SoraStorageUsedBytes: 1000000000, - } - svc := NewSoraQuotaService(userRepo, nil, nil) + svc := NewSoraQuotaService(nil) err := svc.CheckQuota(context.Background(), 1, 999999999) require.NoError(t, err) // 无限制时始终通过 } -func TestCheckQuota_ExactBoundary(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 1024, - SoraStorageUsedBytes: 1024, // 恰好满 - } - svc := NewSoraQuotaService(userRepo, nil, nil) +// ==================== AddUsage/ReleaseUsage ==================== +// 用量追踪已移除,仅记录日志 - // 额外 0 字节不超 - require.NoError(t, svc.CheckQuota(context.Background(), 1, 0)) - // 额外 1 字节超出 - require.Error(t, svc.CheckQuota(context.Background(), 1, 1)) -} +func TestAddUsage_NoTracking(t *testing.T) { + svc := NewSoraQuotaService(nil) -// ==================== AddUsage ==================== - -func TestAddUsage_Success(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.AddUsage(context.Background(), 1, 2048) + err := svc.AddUsage(context.Background(), 1, 1024) require.NoError(t, err) - require.Equal(t, int64(3072), userRepo.users[1].SoraStorageUsedBytes) } func TestAddUsage_ZeroBytes(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - svc := NewSoraQuotaService(userRepo, nil, nil) + svc := NewSoraQuotaService(nil) err := svc.AddUsage(context.Background(), 1, 0) require.NoError(t, err) - require.Equal(t, int64(1024), userRepo.users[1].SoraStorageUsedBytes) // 不变 } -func TestAddUsage_NegativeBytes(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.AddUsage(context.Background(), 1, -100) - require.NoError(t, err) - require.Equal(t, int64(1024), userRepo.users[1].SoraStorageUsedBytes) // 不变 -} - -func TestAddUsage_UserNotFound(t *testing.T) { - userRepo := newStubUserRepoForQuota() - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.AddUsage(context.Background(), 999, 1024) - require.Error(t, err) -} - -func TestAddUsage_UpdateError(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 0} - userRepo.updateErr = fmt.Errorf("db error") - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.AddUsage(context.Background(), 1, 1024) - require.Error(t, err) - require.Contains(t, err.Error(), "update user quota usage") -} - -// ==================== ReleaseUsage ==================== - -func TestReleaseUsage_Success(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 3072} - svc := NewSoraQuotaService(userRepo, nil, nil) +func TestReleaseUsage_NoTracking(t *testing.T) { + svc := NewSoraQuotaService(nil) err := svc.ReleaseUsage(context.Background(), 1, 1024) require.NoError(t, err) - require.Equal(t, int64(2048), userRepo.users[1].SoraStorageUsedBytes) -} - -func TestReleaseUsage_ClampToZero(t *testing.T) { - // 释放量大于已用量时,应 clamp 到 0 - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 500} - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.ReleaseUsage(context.Background(), 1, 1000) - require.NoError(t, err) - require.Equal(t, int64(0), userRepo.users[1].SoraStorageUsedBytes) } func TestReleaseUsage_ZeroBytes(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - svc := NewSoraQuotaService(userRepo, nil, nil) + svc := NewSoraQuotaService(nil) err := svc.ReleaseUsage(context.Background(), 1, 0) require.NoError(t, err) - require.Equal(t, int64(1024), userRepo.users[1].SoraStorageUsedBytes) // 不变 -} - -func TestReleaseUsage_NegativeBytes(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.ReleaseUsage(context.Background(), 1, -50) - require.NoError(t, err) - require.Equal(t, int64(1024), userRepo.users[1].SoraStorageUsedBytes) // 不变 -} - -func TestReleaseUsage_UserNotFound(t *testing.T) { - userRepo := newStubUserRepoForQuota() - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.ReleaseUsage(context.Background(), 999, 1024) - require.Error(t, err) -} - -func TestReleaseUsage_UpdateError(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageUsedBytes: 1024} - userRepo.updateErr = fmt.Errorf("db error") - svc := NewSoraQuotaService(userRepo, nil, nil) - - err := svc.ReleaseUsage(context.Background(), 1, 512) - require.Error(t, err) - require.Contains(t, err.Error(), "update user quota release") } // ==================== GetQuotaFromSettings ==================== func TestGetQuotaFromSettings_NilSettingService(t *testing.T) { - svc := NewSoraQuotaService(nil, nil, nil) + svc := NewSoraQuotaService(nil) require.Equal(t, int64(0), svc.GetQuotaFromSettings(context.Background())) } @@ -394,99 +163,114 @@ func TestGetQuotaFromSettings_WithSettings(t *testing.T) { SettingKeySoraDefaultStorageQuotaBytes: "52428800", // 50MB }) settingService := NewSettingService(settingRepo, &config.Config{}) - svc := NewSoraQuotaService(nil, nil, settingService) + svc := NewSoraQuotaService(settingService) require.Equal(t, int64(52428800), svc.GetQuotaFromSettings(context.Background())) } // ==================== SetUserSoraQuota ==================== +// 用户级配额设置已移除 -func TestSetUserSoraQuota_Success(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ID: 1, SoraStorageQuotaBytes: 0} - - err := SetUserSoraQuota(context.Background(), userRepo, 1, 10*1024*1024) - require.NoError(t, err) - require.Equal(t, int64(10*1024*1024), userRepo.users[1].SoraStorageQuotaBytes) -} - -func TestSetUserSoraQuota_UserNotFound(t *testing.T) { - userRepo := newStubUserRepoForQuota() - err := SetUserSoraQuota(context.Background(), userRepo, 999, 1024) +func TestSetUserSoraQuota_NotSupported(t *testing.T) { + err := SetUserSoraQuota(context.Background(), nil, 1, 1024) require.Error(t, err) + require.Contains(t, err.Error(), "no longer supported") } -// ==================== ParseQuotaBytes ==================== - -func TestParseQuotaBytes(t *testing.T) { - require.Equal(t, int64(1048576), ParseQuotaBytes("1048576")) - require.Equal(t, int64(0), ParseQuotaBytes("")) - require.Equal(t, int64(0), ParseQuotaBytes("abc")) - require.Equal(t, int64(-1), ParseQuotaBytes("-1")) -} - -// ==================== 优先级完整测试 ==================== - -func TestQuotaPriority_UserOverridesGroup(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 5 * 1024 * 1024, - AllowedGroups: []int64{10}, - } - - groupRepo := newStubGroupRepoForQuota() - groupRepo.groups[10] = &Group{ID: 10, SoraStorageQuotaBytes: 20 * 1024 * 1024} - - svc := NewSoraQuotaService(userRepo, groupRepo, nil) - quota, err := svc.GetQuota(context.Background(), 1) - require.NoError(t, err) - require.Equal(t, "user", quota.Source) // 用户级优先 - require.Equal(t, int64(5*1024*1024), quota.QuotaBytes) -} - -func TestQuotaPriority_GroupOverridesSystem(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 0, - AllowedGroups: []int64{10}, - } - - groupRepo := newStubGroupRepoForQuota() - groupRepo.groups[10] = &Group{ID: 10, SoraStorageQuotaBytes: 20 * 1024 * 1024} +// ==================== GetQuota — 字段完整性 ==================== +func TestGetQuota_SystemLevel_AllFields(t *testing.T) { + // 验证 QuotaInfo 所有字段在系统配额模式下正确 settingRepo := newStubSettingRepoForQuota(map[string]string{ SettingKeySoraDefaultStorageQuotaBytes: "104857600", // 100MB }) settingService := NewSettingService(settingRepo, &config.Config{}) + svc := NewSoraQuotaService(settingService) - svc := NewSoraQuotaService(userRepo, groupRepo, settingService) - quota, err := svc.GetQuota(context.Background(), 1) + quota, err := svc.GetQuota(context.Background(), 42) require.NoError(t, err) - require.Equal(t, "group", quota.Source) // 分组级优先于系统 - require.Equal(t, int64(20*1024*1024), quota.QuotaBytes) + require.Equal(t, int64(104857600), quota.QuotaBytes) + require.Equal(t, int64(0), quota.UsedBytes, "用量追踪已移除,UsedBytes 应始终为 0") + require.Equal(t, int64(104857600), quota.AvailableBytes, "无用量追踪时,可用 = 总配额") + require.Equal(t, "system", quota.QuotaSource) + require.Equal(t, "system", quota.Source) } -func TestQuotaPriority_FallbackToSystem(t *testing.T) { - userRepo := newStubUserRepoForQuota() - userRepo.users[1] = &User{ - ID: 1, - SoraStorageQuotaBytes: 0, - AllowedGroups: []int64{10}, - } +func TestGetQuota_Unlimited_AllFields(t *testing.T) { + // 验证无限制模式所有字段 + svc := NewSoraQuotaService(nil) - groupRepo := newStubGroupRepoForQuota() - groupRepo.groups[10] = &Group{ID: 10, SoraStorageQuotaBytes: 0} // 分组无配额 + quota, err := svc.GetQuota(context.Background(), 99) + require.NoError(t, err) + require.Equal(t, int64(0), quota.QuotaBytes) + require.Equal(t, int64(0), quota.UsedBytes) + require.Equal(t, int64(0), quota.AvailableBytes) + require.Equal(t, "unlimited", quota.QuotaSource) + require.Equal(t, "unlimited", quota.Source) +} +// ==================== CheckQuota — 边界条件 ==================== + +func TestCheckQuota_ExactlyAtLimit(t *testing.T) { + // 请求大小 == 配额 → 应通过(<= 判断) settingRepo := newStubSettingRepoForQuota(map[string]string{ - SettingKeySoraDefaultStorageQuotaBytes: "52428800", // 50MB + SettingKeySoraDefaultStorageQuotaBytes: "1024", }) settingService := NewSettingService(settingRepo, &config.Config{}) + svc := NewSoraQuotaService(settingService) - svc := NewSoraQuotaService(userRepo, groupRepo, settingService) - quota, err := svc.GetQuota(context.Background(), 1) - require.NoError(t, err) - require.Equal(t, "system", quota.Source) - require.Equal(t, int64(52428800), quota.QuotaBytes) + err := svc.CheckQuota(context.Background(), 1, 1024) + require.NoError(t, err) // exactly at limit is OK } + +func TestCheckQuota_OneByteOverLimit(t *testing.T) { + settingRepo := newStubSettingRepoForQuota(map[string]string{ + SettingKeySoraDefaultStorageQuotaBytes: "1024", + }) + settingService := NewSettingService(settingRepo, &config.Config{}) + svc := NewSoraQuotaService(settingService) + + err := svc.CheckQuota(context.Background(), 1, 1025) + require.Error(t, err) + + var qe *QuotaExceededError + require.ErrorAs(t, err, &qe) + require.Equal(t, int64(1024), qe.QuotaBytes) + require.Equal(t, int64(0), qe.UsedBytes) +} + +func TestCheckQuota_NegativeBytes(t *testing.T) { + // 负数字节应视为无操作 + svc := NewSoraQuotaService(nil) + err := svc.CheckQuota(context.Background(), 1, -100) + require.NoError(t, err) +} + +// ==================== AddUsage/ReleaseUsage — 边界条件 ==================== + +func TestAddUsage_NegativeBytes(t *testing.T) { + svc := NewSoraQuotaService(nil) + err := svc.AddUsage(context.Background(), 1, -1) + require.NoError(t, err) // <= 0 是 no-op +} + +func TestReleaseUsage_NegativeBytes(t *testing.T) { + svc := NewSoraQuotaService(nil) + err := svc.ReleaseUsage(context.Background(), 1, -999) + require.NoError(t, err) // <= 0 是 no-op +} + +// ==================== QuotaExceededError ==================== + +func TestQuotaExceededError_NilSafe(t *testing.T) { + var e *QuotaExceededError + msg := e.Error() + require.Contains(t, msg, "配额不足") +} + +func TestQuotaExceededError_Format(t *testing.T) { + e := &QuotaExceededError{QuotaBytes: 1024, UsedBytes: 512} + msg := e.Error() + require.Contains(t, msg, "512") + require.Contains(t, msg, "1024") +} \ No newline at end of file diff --git a/backend/internal/service/user.go b/backend/internal/service/user.go index 9f143914..e56d83bf 100644 --- a/backend/internal/service/user.go +++ b/backend/internal/service/user.go @@ -30,10 +30,6 @@ type User struct { TotpEnabled bool // 是否启用 TOTP TotpEnabledAt *time.Time // TOTP 启用时间 - // Sora 存储配额 (从本地版本合并) - SoraStorageQuotaBytes int64 - SoraStorageUsedBytes int64 - APIKeys []APIKey Subscriptions []UserSubscription }