diff --git a/backend/cmd/server/wire.go b/backend/cmd/server/wire.go index b474cfa1aa..da119aaaca 100644 --- a/backend/cmd/server/wire.go +++ b/backend/cmd/server/wire.go @@ -81,6 +81,7 @@ func provideCleanup( schedulerSnapshot *service.SchedulerSnapshotService, tokenRefresh *service.TokenRefreshService, accountExpiry *service.AccountExpiryService, + proxyExpiry *service.ProxyExpiryService, subscriptionExpiry *service.SubscriptionExpiryService, usageCleanup *service.UsageCleanupService, idempotencyCleanup *service.IdempotencyCleanupService, @@ -173,6 +174,10 @@ func provideCleanup( accountExpiry.Stop() return nil }}, + {"ProxyExpiryService", func() error { + proxyExpiry.Stop() + return nil + }}, {"SubscriptionExpiryService", func() error { subscriptionExpiry.Stop() return nil diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 806d31301c..bd6ca2315b 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -261,17 +261,18 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { httpServer := server.ProvideHTTPServer(configConfig, engine) opsMetricsCollector := service.ProvideOpsMetricsCollector(opsRepository, settingRepository, accountRepository, concurrencyService, db, redisClient, configConfig) opsAggregationService := service.ProvideOpsAggregationService(opsRepository, settingRepository, db, redisClient, configConfig) - opsAlertEvaluatorService := service.ProvideOpsAlertEvaluatorService(opsService, opsRepository, emailService, redisClient, configConfig) + opsAlertEvaluatorService := service.ProvideOpsAlertEvaluatorService(opsService, opsRepository, emailService, redisClient, configConfig, proxyRepository) opsCleanupService := service.ProvideOpsCleanupService(opsRepository, db, redisClient, configConfig, channelMonitorService, settingRepository, opsService) opsScheduledReportService := service.ProvideOpsScheduledReportService(opsService, userService, emailService, redisClient, configConfig) tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, compositeTokenCacheInvalidator, schedulerCache, configConfig, tempUnschedCache, privacyClientFactory, proxyRepository, oAuthRefreshAPI, openAIGatewayService) accountExpiryService := service.ProvideAccountExpiryService(accountRepository) + proxyExpiryService := service.ProvideProxyExpiryService(proxyRepository) subscriptionExpiryService := service.ProvideSubscriptionExpiryService(userSubscriptionRepository, settingRepository, notificationEmailService, leaderLockCache, db) scheduledTestRunnerService := service.ProvideScheduledTestRunnerService(scheduledTestPlanRepository, scheduledTestService, accountTestService, rateLimitService, configConfig) paymentOrderExpiryService := service.ProvidePaymentOrderExpiryService(paymentService, leaderLockCache, db) channelMonitorRunner := service.ProvideChannelMonitorRunner(channelMonitorService, settingService) userPlatformQuotaUsageFlusher := service.ProvideUserPlatformQuotaUsageFlusher(configConfig, billingCache, serviceUserPlatformQuotaRepository, timingWheelService) - v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, schedulerSnapshotService, tokenRefreshService, accountExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, openAIGatewayService, scheduledTestRunnerService, backupService, paymentOrderExpiryService, channelMonitorRunner, userPlatformQuotaUsageFlusher) + v := provideCleanup(client, redisClient, opsMetricsCollector, opsAggregationService, opsAlertEvaluatorService, opsCleanupService, opsScheduledReportService, opsSystemLogSink, schedulerSnapshotService, tokenRefreshService, accountExpiryService, proxyExpiryService, subscriptionExpiryService, usageCleanupService, idempotencyCleanupService, pricingService, emailQueueService, billingCacheService, usageRecordWorkerPool, subscriptionService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, openAIGatewayService, scheduledTestRunnerService, backupService, paymentOrderExpiryService, channelMonitorRunner, userPlatformQuotaUsageFlusher) application := &Application{ Server: httpServer, Cleanup: v, @@ -309,6 +310,7 @@ func provideCleanup( schedulerSnapshot *service.SchedulerSnapshotService, tokenRefresh *service.TokenRefreshService, accountExpiry *service.AccountExpiryService, + proxyExpiry *service.ProxyExpiryService, subscriptionExpiry *service.SubscriptionExpiryService, usageCleanup *service.UsageCleanupService, idempotencyCleanup *service.IdempotencyCleanupService, @@ -400,6 +402,10 @@ func provideCleanup( accountExpiry.Stop() return nil }}, + {"ProxyExpiryService", func() error { + proxyExpiry.Stop() + return nil + }}, {"SubscriptionExpiryService", func() error { subscriptionExpiry.Stop() return nil diff --git a/backend/cmd/server/wire_gen_test.go b/backend/cmd/server/wire_gen_test.go index 7f4e477317..4a00c1cceb 100644 --- a/backend/cmd/server/wire_gen_test.go +++ b/backend/cmd/server/wire_gen_test.go @@ -40,6 +40,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) { nil, ) accountExpirySvc := service.NewAccountExpiryService(nil, time.Second) + proxyExpirySvc := service.NewProxyExpiryService(nil, time.Second) subscriptionExpirySvc := service.NewSubscriptionExpiryService(nil, time.Second) pricingSvc := service.NewPricingService(cfg, nil) emailQueueSvc := service.NewEmailQueueService(nil, 1) @@ -60,6 +61,7 @@ func TestProvideCleanup_WithMinimalDependencies_NoPanic(t *testing.T) { schedulerSnapshotSvc, tokenRefreshSvc, accountExpirySvc, + proxyExpirySvc, subscriptionExpirySvc, &service.UsageCleanupService{}, idempotencyCleanupSvc, diff --git a/backend/ent/account.go b/backend/ent/account.go index 2dbfc3a278..86e6030613 100644 --- a/backend/ent/account.go +++ b/backend/ent/account.go @@ -39,6 +39,8 @@ type Account struct { Extra map[string]interface{} `json:"extra,omitempty"` // ProxyID holds the value of the "proxy_id" field. ProxyID *int64 `json:"proxy_id,omitempty"` + // Original proxy id replaced by expiry-fallback; for manual revert. NULL = not in fallback. + ProxyFallbackOriginID *int64 `json:"proxy_fallback_origin_id,omitempty"` // Concurrency holds the value of the "concurrency" field. Concurrency int `json:"concurrency,omitempty"` // LoadFactor holds the value of the "load_factor" field. @@ -145,7 +147,7 @@ func (*Account) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullBool) case account.FieldRateMultiplier: values[i] = new(sql.NullFloat64) - case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldLoadFactor, account.FieldPriority: + case account.FieldID, account.FieldProxyID, account.FieldProxyFallbackOriginID, account.FieldConcurrency, account.FieldLoadFactor, account.FieldPriority: values[i] = new(sql.NullInt64) case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldTempUnschedulableReason, account.FieldSessionWindowStatus: values[i] = new(sql.NullString) @@ -239,6 +241,13 @@ func (_m *Account) assignValues(columns []string, values []any) error { _m.ProxyID = new(int64) *_m.ProxyID = value.Int64 } + case account.FieldProxyFallbackOriginID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field proxy_fallback_origin_id", values[i]) + } else if value.Valid { + _m.ProxyFallbackOriginID = new(int64) + *_m.ProxyFallbackOriginID = value.Int64 + } case account.FieldConcurrency: if value, ok := values[i].(*sql.NullInt64); !ok { return fmt.Errorf("unexpected type %T for field concurrency", values[i]) @@ -451,6 +460,11 @@ func (_m *Account) String() string { builder.WriteString(fmt.Sprintf("%v", *v)) } builder.WriteString(", ") + if v := _m.ProxyFallbackOriginID; v != nil { + builder.WriteString("proxy_fallback_origin_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") builder.WriteString("concurrency=") builder.WriteString(fmt.Sprintf("%v", _m.Concurrency)) builder.WriteString(", ") diff --git a/backend/ent/account/account.go b/backend/ent/account/account.go index 4c1346490a..4df894e8d8 100644 --- a/backend/ent/account/account.go +++ b/backend/ent/account/account.go @@ -35,6 +35,8 @@ const ( FieldExtra = "extra" // FieldProxyID holds the string denoting the proxy_id field in the database. FieldProxyID = "proxy_id" + // FieldProxyFallbackOriginID holds the string denoting the proxy_fallback_origin_id field in the database. + FieldProxyFallbackOriginID = "proxy_fallback_origin_id" // FieldConcurrency holds the string denoting the concurrency field in the database. FieldConcurrency = "concurrency" // FieldLoadFactor holds the string denoting the load_factor field in the database. @@ -122,6 +124,7 @@ var Columns = []string{ FieldCredentials, FieldExtra, FieldProxyID, + FieldProxyFallbackOriginID, FieldConcurrency, FieldLoadFactor, FieldPriority, @@ -248,6 +251,11 @@ func ByProxyID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldProxyID, opts...).ToFunc() } +// ByProxyFallbackOriginID orders the results by the proxy_fallback_origin_id field. +func ByProxyFallbackOriginID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProxyFallbackOriginID, opts...).ToFunc() +} + // ByConcurrency orders the results by the concurrency field. func ByConcurrency(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldConcurrency, opts...).ToFunc() diff --git a/backend/ent/account/where.go b/backend/ent/account/where.go index 3749b45c55..543624f1ec 100644 --- a/backend/ent/account/where.go +++ b/backend/ent/account/where.go @@ -95,6 +95,11 @@ func ProxyID(v int64) predicate.Account { return predicate.Account(sql.FieldEQ(FieldProxyID, v)) } +// ProxyFallbackOriginID applies equality check predicate on the "proxy_fallback_origin_id" field. It's identical to ProxyFallbackOriginIDEQ. +func ProxyFallbackOriginID(v int64) predicate.Account { + return predicate.Account(sql.FieldEQ(FieldProxyFallbackOriginID, v)) +} + // Concurrency applies equality check predicate on the "concurrency" field. It's identical to ConcurrencyEQ. func Concurrency(v int) predicate.Account { return predicate.Account(sql.FieldEQ(FieldConcurrency, v)) @@ -615,6 +620,56 @@ func ProxyIDNotNil() predicate.Account { return predicate.Account(sql.FieldNotNull(FieldProxyID)) } +// ProxyFallbackOriginIDEQ applies the EQ predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDEQ(v int64) predicate.Account { + return predicate.Account(sql.FieldEQ(FieldProxyFallbackOriginID, v)) +} + +// ProxyFallbackOriginIDNEQ applies the NEQ predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDNEQ(v int64) predicate.Account { + return predicate.Account(sql.FieldNEQ(FieldProxyFallbackOriginID, v)) +} + +// ProxyFallbackOriginIDIn applies the In predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDIn(vs ...int64) predicate.Account { + return predicate.Account(sql.FieldIn(FieldProxyFallbackOriginID, vs...)) +} + +// ProxyFallbackOriginIDNotIn applies the NotIn predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDNotIn(vs ...int64) predicate.Account { + return predicate.Account(sql.FieldNotIn(FieldProxyFallbackOriginID, vs...)) +} + +// ProxyFallbackOriginIDGT applies the GT predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDGT(v int64) predicate.Account { + return predicate.Account(sql.FieldGT(FieldProxyFallbackOriginID, v)) +} + +// ProxyFallbackOriginIDGTE applies the GTE predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDGTE(v int64) predicate.Account { + return predicate.Account(sql.FieldGTE(FieldProxyFallbackOriginID, v)) +} + +// ProxyFallbackOriginIDLT applies the LT predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDLT(v int64) predicate.Account { + return predicate.Account(sql.FieldLT(FieldProxyFallbackOriginID, v)) +} + +// ProxyFallbackOriginIDLTE applies the LTE predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDLTE(v int64) predicate.Account { + return predicate.Account(sql.FieldLTE(FieldProxyFallbackOriginID, v)) +} + +// ProxyFallbackOriginIDIsNil applies the IsNil predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDIsNil() predicate.Account { + return predicate.Account(sql.FieldIsNull(FieldProxyFallbackOriginID)) +} + +// ProxyFallbackOriginIDNotNil applies the NotNil predicate on the "proxy_fallback_origin_id" field. +func ProxyFallbackOriginIDNotNil() predicate.Account { + return predicate.Account(sql.FieldNotNull(FieldProxyFallbackOriginID)) +} + // ConcurrencyEQ applies the EQ predicate on the "concurrency" field. func ConcurrencyEQ(v int) predicate.Account { return predicate.Account(sql.FieldEQ(FieldConcurrency, v)) diff --git a/backend/ent/account_create.go b/backend/ent/account_create.go index d6046c7977..2c3a7bc307 100644 --- a/backend/ent/account_create.go +++ b/backend/ent/account_create.go @@ -125,6 +125,20 @@ func (_c *AccountCreate) SetNillableProxyID(v *int64) *AccountCreate { return _c } +// SetProxyFallbackOriginID sets the "proxy_fallback_origin_id" field. +func (_c *AccountCreate) SetProxyFallbackOriginID(v int64) *AccountCreate { + _c.mutation.SetProxyFallbackOriginID(v) + return _c +} + +// SetNillableProxyFallbackOriginID sets the "proxy_fallback_origin_id" field if the given value is not nil. +func (_c *AccountCreate) SetNillableProxyFallbackOriginID(v *int64) *AccountCreate { + if v != nil { + _c.SetProxyFallbackOriginID(*v) + } + return _c +} + // SetConcurrency sets the "concurrency" field. func (_c *AccountCreate) SetConcurrency(v int) *AccountCreate { _c.mutation.SetConcurrency(v) @@ -633,6 +647,10 @@ func (_c *AccountCreate) createSpec() (*Account, *sqlgraph.CreateSpec) { _spec.SetField(account.FieldExtra, field.TypeJSON, value) _node.Extra = value } + if value, ok := _c.mutation.ProxyFallbackOriginID(); ok { + _spec.SetField(account.FieldProxyFallbackOriginID, field.TypeInt64, value) + _node.ProxyFallbackOriginID = &value + } if value, ok := _c.mutation.Concurrency(); ok { _spec.SetField(account.FieldConcurrency, field.TypeInt, value) _node.Concurrency = value @@ -936,6 +954,30 @@ func (u *AccountUpsert) ClearProxyID() *AccountUpsert { return u } +// SetProxyFallbackOriginID sets the "proxy_fallback_origin_id" field. +func (u *AccountUpsert) SetProxyFallbackOriginID(v int64) *AccountUpsert { + u.Set(account.FieldProxyFallbackOriginID, v) + return u +} + +// UpdateProxyFallbackOriginID sets the "proxy_fallback_origin_id" field to the value that was provided on create. +func (u *AccountUpsert) UpdateProxyFallbackOriginID() *AccountUpsert { + u.SetExcluded(account.FieldProxyFallbackOriginID) + return u +} + +// AddProxyFallbackOriginID adds v to the "proxy_fallback_origin_id" field. +func (u *AccountUpsert) AddProxyFallbackOriginID(v int64) *AccountUpsert { + u.Add(account.FieldProxyFallbackOriginID, v) + return u +} + +// ClearProxyFallbackOriginID clears the value of the "proxy_fallback_origin_id" field. +func (u *AccountUpsert) ClearProxyFallbackOriginID() *AccountUpsert { + u.SetNull(account.FieldProxyFallbackOriginID) + return u +} + // SetConcurrency sets the "concurrency" field. func (u *AccountUpsert) SetConcurrency(v int) *AccountUpsert { u.Set(account.FieldConcurrency, v) @@ -1440,6 +1482,34 @@ func (u *AccountUpsertOne) ClearProxyID() *AccountUpsertOne { }) } +// SetProxyFallbackOriginID sets the "proxy_fallback_origin_id" field. +func (u *AccountUpsertOne) SetProxyFallbackOriginID(v int64) *AccountUpsertOne { + return u.Update(func(s *AccountUpsert) { + s.SetProxyFallbackOriginID(v) + }) +} + +// AddProxyFallbackOriginID adds v to the "proxy_fallback_origin_id" field. +func (u *AccountUpsertOne) AddProxyFallbackOriginID(v int64) *AccountUpsertOne { + return u.Update(func(s *AccountUpsert) { + s.AddProxyFallbackOriginID(v) + }) +} + +// UpdateProxyFallbackOriginID sets the "proxy_fallback_origin_id" field to the value that was provided on create. +func (u *AccountUpsertOne) UpdateProxyFallbackOriginID() *AccountUpsertOne { + return u.Update(func(s *AccountUpsert) { + s.UpdateProxyFallbackOriginID() + }) +} + +// ClearProxyFallbackOriginID clears the value of the "proxy_fallback_origin_id" field. +func (u *AccountUpsertOne) ClearProxyFallbackOriginID() *AccountUpsertOne { + return u.Update(func(s *AccountUpsert) { + s.ClearProxyFallbackOriginID() + }) +} + // SetConcurrency sets the "concurrency" field. func (u *AccountUpsertOne) SetConcurrency(v int) *AccountUpsertOne { return u.Update(func(s *AccountUpsert) { @@ -2162,6 +2232,34 @@ func (u *AccountUpsertBulk) ClearProxyID() *AccountUpsertBulk { }) } +// SetProxyFallbackOriginID sets the "proxy_fallback_origin_id" field. +func (u *AccountUpsertBulk) SetProxyFallbackOriginID(v int64) *AccountUpsertBulk { + return u.Update(func(s *AccountUpsert) { + s.SetProxyFallbackOriginID(v) + }) +} + +// AddProxyFallbackOriginID adds v to the "proxy_fallback_origin_id" field. +func (u *AccountUpsertBulk) AddProxyFallbackOriginID(v int64) *AccountUpsertBulk { + return u.Update(func(s *AccountUpsert) { + s.AddProxyFallbackOriginID(v) + }) +} + +// UpdateProxyFallbackOriginID sets the "proxy_fallback_origin_id" field to the value that was provided on create. +func (u *AccountUpsertBulk) UpdateProxyFallbackOriginID() *AccountUpsertBulk { + return u.Update(func(s *AccountUpsert) { + s.UpdateProxyFallbackOriginID() + }) +} + +// ClearProxyFallbackOriginID clears the value of the "proxy_fallback_origin_id" field. +func (u *AccountUpsertBulk) ClearProxyFallbackOriginID() *AccountUpsertBulk { + return u.Update(func(s *AccountUpsert) { + s.ClearProxyFallbackOriginID() + }) +} + // SetConcurrency sets the "concurrency" field. func (u *AccountUpsertBulk) SetConcurrency(v int) *AccountUpsertBulk { return u.Update(func(s *AccountUpsert) { diff --git a/backend/ent/account_update.go b/backend/ent/account_update.go index 6f443c65e0..3453c8b10f 100644 --- a/backend/ent/account_update.go +++ b/backend/ent/account_update.go @@ -151,6 +151,33 @@ func (_u *AccountUpdate) ClearProxyID() *AccountUpdate { return _u } +// SetProxyFallbackOriginID sets the "proxy_fallback_origin_id" field. +func (_u *AccountUpdate) SetProxyFallbackOriginID(v int64) *AccountUpdate { + _u.mutation.ResetProxyFallbackOriginID() + _u.mutation.SetProxyFallbackOriginID(v) + return _u +} + +// SetNillableProxyFallbackOriginID sets the "proxy_fallback_origin_id" field if the given value is not nil. +func (_u *AccountUpdate) SetNillableProxyFallbackOriginID(v *int64) *AccountUpdate { + if v != nil { + _u.SetProxyFallbackOriginID(*v) + } + return _u +} + +// AddProxyFallbackOriginID adds value to the "proxy_fallback_origin_id" field. +func (_u *AccountUpdate) AddProxyFallbackOriginID(v int64) *AccountUpdate { + _u.mutation.AddProxyFallbackOriginID(v) + return _u +} + +// ClearProxyFallbackOriginID clears the value of the "proxy_fallback_origin_id" field. +func (_u *AccountUpdate) ClearProxyFallbackOriginID() *AccountUpdate { + _u.mutation.ClearProxyFallbackOriginID() + return _u +} + // SetConcurrency sets the "concurrency" field. func (_u *AccountUpdate) SetConcurrency(v int) *AccountUpdate { _u.mutation.ResetConcurrency() @@ -705,6 +732,15 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.Extra(); ok { _spec.SetField(account.FieldExtra, field.TypeJSON, value) } + if value, ok := _u.mutation.ProxyFallbackOriginID(); ok { + _spec.SetField(account.FieldProxyFallbackOriginID, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedProxyFallbackOriginID(); ok { + _spec.AddField(account.FieldProxyFallbackOriginID, field.TypeInt64, value) + } + if _u.mutation.ProxyFallbackOriginIDCleared() { + _spec.ClearField(account.FieldProxyFallbackOriginID, field.TypeInt64) + } if value, ok := _u.mutation.Concurrency(); ok { _spec.SetField(account.FieldConcurrency, field.TypeInt, value) } @@ -1078,6 +1114,33 @@ func (_u *AccountUpdateOne) ClearProxyID() *AccountUpdateOne { return _u } +// SetProxyFallbackOriginID sets the "proxy_fallback_origin_id" field. +func (_u *AccountUpdateOne) SetProxyFallbackOriginID(v int64) *AccountUpdateOne { + _u.mutation.ResetProxyFallbackOriginID() + _u.mutation.SetProxyFallbackOriginID(v) + return _u +} + +// SetNillableProxyFallbackOriginID sets the "proxy_fallback_origin_id" field if the given value is not nil. +func (_u *AccountUpdateOne) SetNillableProxyFallbackOriginID(v *int64) *AccountUpdateOne { + if v != nil { + _u.SetProxyFallbackOriginID(*v) + } + return _u +} + +// AddProxyFallbackOriginID adds value to the "proxy_fallback_origin_id" field. +func (_u *AccountUpdateOne) AddProxyFallbackOriginID(v int64) *AccountUpdateOne { + _u.mutation.AddProxyFallbackOriginID(v) + return _u +} + +// ClearProxyFallbackOriginID clears the value of the "proxy_fallback_origin_id" field. +func (_u *AccountUpdateOne) ClearProxyFallbackOriginID() *AccountUpdateOne { + _u.mutation.ClearProxyFallbackOriginID() + return _u +} + // SetConcurrency sets the "concurrency" field. func (_u *AccountUpdateOne) SetConcurrency(v int) *AccountUpdateOne { _u.mutation.ResetConcurrency() @@ -1662,6 +1725,15 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er if value, ok := _u.mutation.Extra(); ok { _spec.SetField(account.FieldExtra, field.TypeJSON, value) } + if value, ok := _u.mutation.ProxyFallbackOriginID(); ok { + _spec.SetField(account.FieldProxyFallbackOriginID, field.TypeInt64, value) + } + if value, ok := _u.mutation.AddedProxyFallbackOriginID(); ok { + _spec.AddField(account.FieldProxyFallbackOriginID, field.TypeInt64, value) + } + if _u.mutation.ProxyFallbackOriginIDCleared() { + _spec.ClearField(account.FieldProxyFallbackOriginID, field.TypeInt64) + } if value, ok := _u.mutation.Concurrency(); ok { _spec.SetField(account.FieldConcurrency, field.TypeInt, value) } diff --git a/backend/ent/client.go b/backend/ent/client.go index 06b53c781a..a0dab61036 100644 --- a/backend/ent/client.go +++ b/backend/ent/client.go @@ -3979,6 +3979,22 @@ func (c *ProxyClient) QueryAccounts(_m *Proxy) *AccountQuery { return query } +// QueryBackupProxy queries the backup_proxy edge of a Proxy. +func (c *ProxyClient) QueryBackupProxy(_m *Proxy) *ProxyQuery { + query := (&ProxyClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(proxy.Table, proxy.FieldID, id), + sqlgraph.To(proxy.Table, proxy.FieldID), + sqlgraph.Edge(sqlgraph.O2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *ProxyClient) Hooks() []Hook { hooks := c.hooks.Proxy diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index 7abe4c601e..f235031c34 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -105,6 +105,7 @@ var ( {Name: "type", Type: field.TypeString, Size: 20}, {Name: "credentials", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}}, {Name: "extra", Type: field.TypeJSON, SchemaType: map[string]string{"postgres": "jsonb"}}, + {Name: "proxy_fallback_origin_id", Type: field.TypeInt64, Nullable: true}, {Name: "concurrency", Type: field.TypeInt, Default: 3}, {Name: "load_factor", Type: field.TypeInt, Nullable: true}, {Name: "priority", Type: field.TypeInt, Default: 50}, @@ -133,7 +134,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "accounts_proxies_proxy", - Columns: []*schema.Column{AccountsColumns[28]}, + Columns: []*schema.Column{AccountsColumns[29]}, RefColumns: []*schema.Column{ProxiesColumns[0]}, OnDelete: schema.SetNull, }, @@ -152,52 +153,52 @@ var ( { Name: "account_status", Unique: false, - Columns: []*schema.Column{AccountsColumns[14]}, + Columns: []*schema.Column{AccountsColumns[15]}, }, { Name: "account_proxy_id", Unique: false, - Columns: []*schema.Column{AccountsColumns[28]}, + Columns: []*schema.Column{AccountsColumns[29]}, }, { Name: "account_priority", Unique: false, - Columns: []*schema.Column{AccountsColumns[12]}, + Columns: []*schema.Column{AccountsColumns[13]}, }, { Name: "account_last_used_at", Unique: false, - Columns: []*schema.Column{AccountsColumns[16]}, + Columns: []*schema.Column{AccountsColumns[17]}, }, { Name: "account_schedulable", Unique: false, - Columns: []*schema.Column{AccountsColumns[19]}, + Columns: []*schema.Column{AccountsColumns[20]}, }, { Name: "account_rate_limited_at", Unique: false, - Columns: []*schema.Column{AccountsColumns[20]}, + Columns: []*schema.Column{AccountsColumns[21]}, }, { Name: "account_rate_limit_reset_at", Unique: false, - Columns: []*schema.Column{AccountsColumns[21]}, + Columns: []*schema.Column{AccountsColumns[22]}, }, { Name: "account_overload_until", Unique: false, - Columns: []*schema.Column{AccountsColumns[22]}, + Columns: []*schema.Column{AccountsColumns[23]}, }, { Name: "account_platform_priority", Unique: false, - Columns: []*schema.Column{AccountsColumns[6], AccountsColumns[12]}, + Columns: []*schema.Column{AccountsColumns[6], AccountsColumns[13]}, }, { Name: "account_priority_status", Unique: false, - Columns: []*schema.Column{AccountsColumns[12], AccountsColumns[14]}, + Columns: []*schema.Column{AccountsColumns[13], AccountsColumns[15]}, }, { Name: "account_deleted_at", @@ -1104,12 +1105,24 @@ var ( {Name: "username", Type: field.TypeString, Nullable: true, Size: 100}, {Name: "password", Type: field.TypeString, Nullable: true, Size: 100}, {Name: "status", Type: field.TypeString, Size: 20, Default: "active"}, + {Name: "expires_at", Type: field.TypeTime, Nullable: true}, + {Name: "fallback_mode", Type: field.TypeString, Size: 20, Default: "none"}, + {Name: "expiry_warn_days", Type: field.TypeInt, Default: 7}, + {Name: "backup_proxy_id", Type: field.TypeInt64, Unique: true, Nullable: true}, } // ProxiesTable holds the schema information for the "proxies" table. ProxiesTable = &schema.Table{ Name: "proxies", Columns: ProxiesColumns, PrimaryKey: []*schema.Column{ProxiesColumns[0]}, + ForeignKeys: []*schema.ForeignKey{ + { + Symbol: "proxies_proxies_backup_proxy", + Columns: []*schema.Column{ProxiesColumns[14]}, + RefColumns: []*schema.Column{ProxiesColumns[0]}, + OnDelete: schema.SetNull, + }, + }, Indexes: []*schema.Index{ { Name: "proxy_status", @@ -1121,6 +1134,16 @@ var ( Unique: false, Columns: []*schema.Column{ProxiesColumns[3]}, }, + { + Name: "proxy_expires_at", + Unique: false, + Columns: []*schema.Column{ProxiesColumns[11]}, + }, + { + Name: "proxy_backup_proxy_id", + Unique: false, + Columns: []*schema.Column{ProxiesColumns[14]}, + }, }, } // RedeemCodesColumns holds the columns for the "redeem_codes" table. @@ -1871,6 +1894,7 @@ func init() { PromoCodeUsagesTable.Annotation = &entsql.Annotation{ Table: "promo_code_usages", } + ProxiesTable.ForeignKeys[0].RefTable = ProxiesTable ProxiesTable.Annotation = &entsql.Annotation{ Table: "proxies", } diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 003e25d501..9ce8e6362e 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -2274,52 +2274,54 @@ func (m *APIKeyMutation) ResetEdge(name string) error { // AccountMutation represents an operation that mutates the Account nodes in the graph. type AccountMutation struct { config - op Op - typ string - id *int64 - created_at *time.Time - updated_at *time.Time - deleted_at *time.Time - name *string - notes *string - platform *string - _type *string - credentials *map[string]interface{} - extra *map[string]interface{} - concurrency *int - addconcurrency *int - load_factor *int - addload_factor *int - priority *int - addpriority *int - rate_multiplier *float64 - addrate_multiplier *float64 - status *string - error_message *string - last_used_at *time.Time - expires_at *time.Time - auto_pause_on_expired *bool - schedulable *bool - rate_limited_at *time.Time - rate_limit_reset_at *time.Time - overload_until *time.Time - temp_unschedulable_until *time.Time - temp_unschedulable_reason *string - session_window_start *time.Time - session_window_end *time.Time - session_window_status *string - clearedFields map[string]struct{} - groups map[int64]struct{} - removedgroups map[int64]struct{} - clearedgroups bool - proxy *int64 - clearedproxy bool - usage_logs map[int64]struct{} - removedusage_logs map[int64]struct{} - clearedusage_logs bool - done bool - oldValue func(context.Context) (*Account, error) - predicates []predicate.Account + op Op + typ string + id *int64 + created_at *time.Time + updated_at *time.Time + deleted_at *time.Time + name *string + notes *string + platform *string + _type *string + credentials *map[string]interface{} + extra *map[string]interface{} + proxy_fallback_origin_id *int64 + addproxy_fallback_origin_id *int64 + concurrency *int + addconcurrency *int + load_factor *int + addload_factor *int + priority *int + addpriority *int + rate_multiplier *float64 + addrate_multiplier *float64 + status *string + error_message *string + last_used_at *time.Time + expires_at *time.Time + auto_pause_on_expired *bool + schedulable *bool + rate_limited_at *time.Time + rate_limit_reset_at *time.Time + overload_until *time.Time + temp_unschedulable_until *time.Time + temp_unschedulable_reason *string + session_window_start *time.Time + session_window_end *time.Time + session_window_status *string + clearedFields map[string]struct{} + groups map[int64]struct{} + removedgroups map[int64]struct{} + clearedgroups bool + proxy *int64 + clearedproxy bool + usage_logs map[int64]struct{} + removedusage_logs map[int64]struct{} + clearedusage_logs bool + done bool + oldValue func(context.Context) (*Account, error) + predicates []predicate.Account } var _ ent.Mutation = (*AccountMutation)(nil) @@ -2819,6 +2821,76 @@ func (m *AccountMutation) ResetProxyID() { delete(m.clearedFields, account.FieldProxyID) } +// SetProxyFallbackOriginID sets the "proxy_fallback_origin_id" field. +func (m *AccountMutation) SetProxyFallbackOriginID(i int64) { + m.proxy_fallback_origin_id = &i + m.addproxy_fallback_origin_id = nil +} + +// ProxyFallbackOriginID returns the value of the "proxy_fallback_origin_id" field in the mutation. +func (m *AccountMutation) ProxyFallbackOriginID() (r int64, exists bool) { + v := m.proxy_fallback_origin_id + if v == nil { + return + } + return *v, true +} + +// OldProxyFallbackOriginID returns the old "proxy_fallback_origin_id" field's value of the Account entity. +// If the Account 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 *AccountMutation) OldProxyFallbackOriginID(ctx context.Context) (v *int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProxyFallbackOriginID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProxyFallbackOriginID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProxyFallbackOriginID: %w", err) + } + return oldValue.ProxyFallbackOriginID, nil +} + +// AddProxyFallbackOriginID adds i to the "proxy_fallback_origin_id" field. +func (m *AccountMutation) AddProxyFallbackOriginID(i int64) { + if m.addproxy_fallback_origin_id != nil { + *m.addproxy_fallback_origin_id += i + } else { + m.addproxy_fallback_origin_id = &i + } +} + +// AddedProxyFallbackOriginID returns the value that was added to the "proxy_fallback_origin_id" field in this mutation. +func (m *AccountMutation) AddedProxyFallbackOriginID() (r int64, exists bool) { + v := m.addproxy_fallback_origin_id + if v == nil { + return + } + return *v, true +} + +// ClearProxyFallbackOriginID clears the value of the "proxy_fallback_origin_id" field. +func (m *AccountMutation) ClearProxyFallbackOriginID() { + m.proxy_fallback_origin_id = nil + m.addproxy_fallback_origin_id = nil + m.clearedFields[account.FieldProxyFallbackOriginID] = struct{}{} +} + +// ProxyFallbackOriginIDCleared returns if the "proxy_fallback_origin_id" field was cleared in this mutation. +func (m *AccountMutation) ProxyFallbackOriginIDCleared() bool { + _, ok := m.clearedFields[account.FieldProxyFallbackOriginID] + return ok +} + +// ResetProxyFallbackOriginID resets all changes to the "proxy_fallback_origin_id" field. +func (m *AccountMutation) ResetProxyFallbackOriginID() { + m.proxy_fallback_origin_id = nil + m.addproxy_fallback_origin_id = nil + delete(m.clearedFields, account.FieldProxyFallbackOriginID) +} + // SetConcurrency sets the "concurrency" field. func (m *AccountMutation) SetConcurrency(i int) { m.concurrency = &i @@ -3873,7 +3945,7 @@ func (m *AccountMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *AccountMutation) Fields() []string { - fields := make([]string, 0, 28) + fields := make([]string, 0, 29) if m.created_at != nil { fields = append(fields, account.FieldCreatedAt) } @@ -3904,6 +3976,9 @@ func (m *AccountMutation) Fields() []string { if m.proxy != nil { fields = append(fields, account.FieldProxyID) } + if m.proxy_fallback_origin_id != nil { + fields = append(fields, account.FieldProxyFallbackOriginID) + } if m.concurrency != nil { fields = append(fields, account.FieldConcurrency) } @@ -3986,6 +4061,8 @@ func (m *AccountMutation) Field(name string) (ent.Value, bool) { return m.Extra() case account.FieldProxyID: return m.ProxyID() + case account.FieldProxyFallbackOriginID: + return m.ProxyFallbackOriginID() case account.FieldConcurrency: return m.Concurrency() case account.FieldLoadFactor: @@ -4051,6 +4128,8 @@ func (m *AccountMutation) OldField(ctx context.Context, name string) (ent.Value, return m.OldExtra(ctx) case account.FieldProxyID: return m.OldProxyID(ctx) + case account.FieldProxyFallbackOriginID: + return m.OldProxyFallbackOriginID(ctx) case account.FieldConcurrency: return m.OldConcurrency(ctx) case account.FieldLoadFactor: @@ -4166,6 +4245,13 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error { } m.SetProxyID(v) return nil + case account.FieldProxyFallbackOriginID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProxyFallbackOriginID(v) + return nil case account.FieldConcurrency: v, ok := value.(int) if !ok { @@ -4300,6 +4386,9 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error { // this mutation. func (m *AccountMutation) AddedFields() []string { var fields []string + if m.addproxy_fallback_origin_id != nil { + fields = append(fields, account.FieldProxyFallbackOriginID) + } if m.addconcurrency != nil { fields = append(fields, account.FieldConcurrency) } @@ -4320,6 +4409,8 @@ func (m *AccountMutation) AddedFields() []string { // was not set, or was not defined in the schema. func (m *AccountMutation) AddedField(name string) (ent.Value, bool) { switch name { + case account.FieldProxyFallbackOriginID: + return m.AddedProxyFallbackOriginID() case account.FieldConcurrency: return m.AddedConcurrency() case account.FieldLoadFactor: @@ -4337,6 +4428,13 @@ func (m *AccountMutation) AddedField(name string) (ent.Value, bool) { // type. func (m *AccountMutation) AddField(name string, value ent.Value) error { switch name { + case account.FieldProxyFallbackOriginID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddProxyFallbackOriginID(v) + return nil case account.FieldConcurrency: v, ok := value.(int) if !ok { @@ -4382,6 +4480,9 @@ func (m *AccountMutation) ClearedFields() []string { if m.FieldCleared(account.FieldProxyID) { fields = append(fields, account.FieldProxyID) } + if m.FieldCleared(account.FieldProxyFallbackOriginID) { + fields = append(fields, account.FieldProxyFallbackOriginID) + } if m.FieldCleared(account.FieldLoadFactor) { fields = append(fields, account.FieldLoadFactor) } @@ -4441,6 +4542,9 @@ func (m *AccountMutation) ClearField(name string) error { case account.FieldProxyID: m.ClearProxyID() return nil + case account.FieldProxyFallbackOriginID: + m.ClearProxyFallbackOriginID() + return nil case account.FieldLoadFactor: m.ClearLoadFactor() return nil @@ -4515,6 +4619,9 @@ func (m *AccountMutation) ResetField(name string) error { case account.FieldProxyID: m.ResetProxyID() return nil + case account.FieldProxyFallbackOriginID: + m.ResetProxyFallbackOriginID() + return nil case account.FieldConcurrency: m.ResetConcurrency() return nil @@ -27754,27 +27861,33 @@ func (m *PromoCodeUsageMutation) ResetEdge(name string) error { // ProxyMutation represents an operation that mutates the Proxy nodes in the graph. type ProxyMutation struct { config - op Op - typ string - id *int64 - created_at *time.Time - updated_at *time.Time - deleted_at *time.Time - name *string - protocol *string - host *string - port *int - addport *int - username *string - password *string - status *string - clearedFields map[string]struct{} - accounts map[int64]struct{} - removedaccounts map[int64]struct{} - clearedaccounts bool - done bool - oldValue func(context.Context) (*Proxy, error) - predicates []predicate.Proxy + op Op + typ string + id *int64 + created_at *time.Time + updated_at *time.Time + deleted_at *time.Time + name *string + protocol *string + host *string + port *int + addport *int + username *string + password *string + status *string + expires_at *time.Time + fallback_mode *string + expiry_warn_days *int + addexpiry_warn_days *int + clearedFields map[string]struct{} + accounts map[int64]struct{} + removedaccounts map[int64]struct{} + clearedaccounts bool + backup_proxy *int64 + clearedbackup_proxy bool + done bool + oldValue func(context.Context) (*Proxy, error) + predicates []predicate.Proxy } var _ ent.Mutation = (*ProxyMutation)(nil) @@ -28294,6 +28407,196 @@ func (m *ProxyMutation) ResetStatus() { m.status = nil } +// SetExpiresAt sets the "expires_at" field. +func (m *ProxyMutation) SetExpiresAt(t time.Time) { + m.expires_at = &t +} + +// ExpiresAt returns the value of the "expires_at" field in the mutation. +func (m *ProxyMutation) ExpiresAt() (r time.Time, exists bool) { + v := m.expires_at + if v == nil { + return + } + return *v, true +} + +// OldExpiresAt returns the old "expires_at" field's value of the Proxy entity. +// If the Proxy 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 *ProxyMutation) OldExpiresAt(ctx context.Context) (v *time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiresAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiresAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiresAt: %w", err) + } + return oldValue.ExpiresAt, nil +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (m *ProxyMutation) ClearExpiresAt() { + m.expires_at = nil + m.clearedFields[proxy.FieldExpiresAt] = struct{}{} +} + +// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation. +func (m *ProxyMutation) ExpiresAtCleared() bool { + _, ok := m.clearedFields[proxy.FieldExpiresAt] + return ok +} + +// ResetExpiresAt resets all changes to the "expires_at" field. +func (m *ProxyMutation) ResetExpiresAt() { + m.expires_at = nil + delete(m.clearedFields, proxy.FieldExpiresAt) +} + +// SetFallbackMode sets the "fallback_mode" field. +func (m *ProxyMutation) SetFallbackMode(s string) { + m.fallback_mode = &s +} + +// FallbackMode returns the value of the "fallback_mode" field in the mutation. +func (m *ProxyMutation) FallbackMode() (r string, exists bool) { + v := m.fallback_mode + if v == nil { + return + } + return *v, true +} + +// OldFallbackMode returns the old "fallback_mode" field's value of the Proxy entity. +// If the Proxy 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 *ProxyMutation) OldFallbackMode(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldFallbackMode is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldFallbackMode requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldFallbackMode: %w", err) + } + return oldValue.FallbackMode, nil +} + +// ResetFallbackMode resets all changes to the "fallback_mode" field. +func (m *ProxyMutation) ResetFallbackMode() { + m.fallback_mode = nil +} + +// SetBackupProxyID sets the "backup_proxy_id" field. +func (m *ProxyMutation) SetBackupProxyID(i int64) { + m.backup_proxy = &i +} + +// BackupProxyID returns the value of the "backup_proxy_id" field in the mutation. +func (m *ProxyMutation) BackupProxyID() (r int64, exists bool) { + v := m.backup_proxy + if v == nil { + return + } + return *v, true +} + +// OldBackupProxyID returns the old "backup_proxy_id" field's value of the Proxy entity. +// If the Proxy 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 *ProxyMutation) OldBackupProxyID(ctx context.Context) (v *int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldBackupProxyID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldBackupProxyID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldBackupProxyID: %w", err) + } + return oldValue.BackupProxyID, nil +} + +// ClearBackupProxyID clears the value of the "backup_proxy_id" field. +func (m *ProxyMutation) ClearBackupProxyID() { + m.backup_proxy = nil + m.clearedFields[proxy.FieldBackupProxyID] = struct{}{} +} + +// BackupProxyIDCleared returns if the "backup_proxy_id" field was cleared in this mutation. +func (m *ProxyMutation) BackupProxyIDCleared() bool { + _, ok := m.clearedFields[proxy.FieldBackupProxyID] + return ok +} + +// ResetBackupProxyID resets all changes to the "backup_proxy_id" field. +func (m *ProxyMutation) ResetBackupProxyID() { + m.backup_proxy = nil + delete(m.clearedFields, proxy.FieldBackupProxyID) +} + +// SetExpiryWarnDays sets the "expiry_warn_days" field. +func (m *ProxyMutation) SetExpiryWarnDays(i int) { + m.expiry_warn_days = &i + m.addexpiry_warn_days = nil +} + +// ExpiryWarnDays returns the value of the "expiry_warn_days" field in the mutation. +func (m *ProxyMutation) ExpiryWarnDays() (r int, exists bool) { + v := m.expiry_warn_days + if v == nil { + return + } + return *v, true +} + +// OldExpiryWarnDays returns the old "expiry_warn_days" field's value of the Proxy entity. +// If the Proxy 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 *ProxyMutation) OldExpiryWarnDays(ctx context.Context) (v int, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldExpiryWarnDays is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldExpiryWarnDays requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldExpiryWarnDays: %w", err) + } + return oldValue.ExpiryWarnDays, nil +} + +// AddExpiryWarnDays adds i to the "expiry_warn_days" field. +func (m *ProxyMutation) AddExpiryWarnDays(i int) { + if m.addexpiry_warn_days != nil { + *m.addexpiry_warn_days += i + } else { + m.addexpiry_warn_days = &i + } +} + +// AddedExpiryWarnDays returns the value that was added to the "expiry_warn_days" field in this mutation. +func (m *ProxyMutation) AddedExpiryWarnDays() (r int, exists bool) { + v := m.addexpiry_warn_days + if v == nil { + return + } + return *v, true +} + +// ResetExpiryWarnDays resets all changes to the "expiry_warn_days" field. +func (m *ProxyMutation) ResetExpiryWarnDays() { + m.expiry_warn_days = nil + m.addexpiry_warn_days = nil +} + // AddAccountIDs adds the "accounts" edge to the Account entity by ids. func (m *ProxyMutation) AddAccountIDs(ids ...int64) { if m.accounts == nil { @@ -28348,6 +28651,33 @@ func (m *ProxyMutation) ResetAccounts() { m.removedaccounts = nil } +// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity. +func (m *ProxyMutation) ClearBackupProxy() { + m.clearedbackup_proxy = true + m.clearedFields[proxy.FieldBackupProxyID] = struct{}{} +} + +// BackupProxyCleared reports if the "backup_proxy" edge to the Proxy entity was cleared. +func (m *ProxyMutation) BackupProxyCleared() bool { + return m.BackupProxyIDCleared() || m.clearedbackup_proxy +} + +// BackupProxyIDs returns the "backup_proxy" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// BackupProxyID instead. It exists only for internal usage by the builders. +func (m *ProxyMutation) BackupProxyIDs() (ids []int64) { + if id := m.backup_proxy; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetBackupProxy resets all changes to the "backup_proxy" edge. +func (m *ProxyMutation) ResetBackupProxy() { + m.backup_proxy = nil + m.clearedbackup_proxy = false +} + // Where appends a list predicates to the ProxyMutation builder. func (m *ProxyMutation) Where(ps ...predicate.Proxy) { m.predicates = append(m.predicates, ps...) @@ -28382,7 +28712,7 @@ func (m *ProxyMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *ProxyMutation) Fields() []string { - fields := make([]string, 0, 10) + fields := make([]string, 0, 14) if m.created_at != nil { fields = append(fields, proxy.FieldCreatedAt) } @@ -28413,6 +28743,18 @@ func (m *ProxyMutation) Fields() []string { if m.status != nil { fields = append(fields, proxy.FieldStatus) } + if m.expires_at != nil { + fields = append(fields, proxy.FieldExpiresAt) + } + if m.fallback_mode != nil { + fields = append(fields, proxy.FieldFallbackMode) + } + if m.backup_proxy != nil { + fields = append(fields, proxy.FieldBackupProxyID) + } + if m.expiry_warn_days != nil { + fields = append(fields, proxy.FieldExpiryWarnDays) + } return fields } @@ -28441,6 +28783,14 @@ func (m *ProxyMutation) Field(name string) (ent.Value, bool) { return m.Password() case proxy.FieldStatus: return m.Status() + case proxy.FieldExpiresAt: + return m.ExpiresAt() + case proxy.FieldFallbackMode: + return m.FallbackMode() + case proxy.FieldBackupProxyID: + return m.BackupProxyID() + case proxy.FieldExpiryWarnDays: + return m.ExpiryWarnDays() } return nil, false } @@ -28470,6 +28820,14 @@ func (m *ProxyMutation) OldField(ctx context.Context, name string) (ent.Value, e return m.OldPassword(ctx) case proxy.FieldStatus: return m.OldStatus(ctx) + case proxy.FieldExpiresAt: + return m.OldExpiresAt(ctx) + case proxy.FieldFallbackMode: + return m.OldFallbackMode(ctx) + case proxy.FieldBackupProxyID: + return m.OldBackupProxyID(ctx) + case proxy.FieldExpiryWarnDays: + return m.OldExpiryWarnDays(ctx) } return nil, fmt.Errorf("unknown Proxy field %s", name) } @@ -28549,6 +28907,34 @@ func (m *ProxyMutation) SetField(name string, value ent.Value) error { } m.SetStatus(v) return nil + case proxy.FieldExpiresAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiresAt(v) + return nil + case proxy.FieldFallbackMode: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetFallbackMode(v) + return nil + case proxy.FieldBackupProxyID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetBackupProxyID(v) + return nil + case proxy.FieldExpiryWarnDays: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetExpiryWarnDays(v) + return nil } return fmt.Errorf("unknown Proxy field %s", name) } @@ -28560,6 +28946,9 @@ func (m *ProxyMutation) AddedFields() []string { if m.addport != nil { fields = append(fields, proxy.FieldPort) } + if m.addexpiry_warn_days != nil { + fields = append(fields, proxy.FieldExpiryWarnDays) + } return fields } @@ -28570,6 +28959,8 @@ func (m *ProxyMutation) AddedField(name string) (ent.Value, bool) { switch name { case proxy.FieldPort: return m.AddedPort() + case proxy.FieldExpiryWarnDays: + return m.AddedExpiryWarnDays() } return nil, false } @@ -28586,6 +28977,13 @@ func (m *ProxyMutation) AddField(name string, value ent.Value) error { } m.AddPort(v) return nil + case proxy.FieldExpiryWarnDays: + v, ok := value.(int) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.AddExpiryWarnDays(v) + return nil } return fmt.Errorf("unknown Proxy numeric field %s", name) } @@ -28603,6 +29001,12 @@ func (m *ProxyMutation) ClearedFields() []string { if m.FieldCleared(proxy.FieldPassword) { fields = append(fields, proxy.FieldPassword) } + if m.FieldCleared(proxy.FieldExpiresAt) { + fields = append(fields, proxy.FieldExpiresAt) + } + if m.FieldCleared(proxy.FieldBackupProxyID) { + fields = append(fields, proxy.FieldBackupProxyID) + } return fields } @@ -28626,6 +29030,12 @@ func (m *ProxyMutation) ClearField(name string) error { case proxy.FieldPassword: m.ClearPassword() return nil + case proxy.FieldExpiresAt: + m.ClearExpiresAt() + return nil + case proxy.FieldBackupProxyID: + m.ClearBackupProxyID() + return nil } return fmt.Errorf("unknown Proxy nullable field %s", name) } @@ -28664,16 +29074,31 @@ func (m *ProxyMutation) ResetField(name string) error { case proxy.FieldStatus: m.ResetStatus() return nil + case proxy.FieldExpiresAt: + m.ResetExpiresAt() + return nil + case proxy.FieldFallbackMode: + m.ResetFallbackMode() + return nil + case proxy.FieldBackupProxyID: + m.ResetBackupProxyID() + return nil + case proxy.FieldExpiryWarnDays: + m.ResetExpiryWarnDays() + return nil } return fmt.Errorf("unknown Proxy field %s", name) } // AddedEdges returns all edge names that were set/added in this mutation. func (m *ProxyMutation) AddedEdges() []string { - edges := make([]string, 0, 1) + edges := make([]string, 0, 2) if m.accounts != nil { edges = append(edges, proxy.EdgeAccounts) } + if m.backup_proxy != nil { + edges = append(edges, proxy.EdgeBackupProxy) + } return edges } @@ -28687,13 +29112,17 @@ func (m *ProxyMutation) AddedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case proxy.EdgeBackupProxy: + if id := m.backup_proxy; id != nil { + return []ent.Value{*id} + } } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *ProxyMutation) RemovedEdges() []string { - edges := make([]string, 0, 1) + edges := make([]string, 0, 2) if m.removedaccounts != nil { edges = append(edges, proxy.EdgeAccounts) } @@ -28716,10 +29145,13 @@ func (m *ProxyMutation) RemovedIDs(name string) []ent.Value { // ClearedEdges returns all edge names that were cleared in this mutation. func (m *ProxyMutation) ClearedEdges() []string { - edges := make([]string, 0, 1) + edges := make([]string, 0, 2) if m.clearedaccounts { edges = append(edges, proxy.EdgeAccounts) } + if m.clearedbackup_proxy { + edges = append(edges, proxy.EdgeBackupProxy) + } return edges } @@ -28729,6 +29161,8 @@ func (m *ProxyMutation) EdgeCleared(name string) bool { switch name { case proxy.EdgeAccounts: return m.clearedaccounts + case proxy.EdgeBackupProxy: + return m.clearedbackup_proxy } return false } @@ -28737,6 +29171,9 @@ func (m *ProxyMutation) EdgeCleared(name string) bool { // if that edge is not defined in the schema. func (m *ProxyMutation) ClearEdge(name string) error { switch name { + case proxy.EdgeBackupProxy: + m.ClearBackupProxy() + return nil } return fmt.Errorf("unknown Proxy unique edge %s", name) } @@ -28748,6 +29185,9 @@ func (m *ProxyMutation) ResetEdge(name string) error { case proxy.EdgeAccounts: m.ResetAccounts() return nil + case proxy.EdgeBackupProxy: + m.ResetBackupProxy() + return nil } return fmt.Errorf("unknown Proxy edge %s", name) } diff --git a/backend/ent/proxy.go b/backend/ent/proxy.go index 5228b73e9e..3dc4491bd1 100644 --- a/backend/ent/proxy.go +++ b/backend/ent/proxy.go @@ -37,6 +37,14 @@ type Proxy struct { Password *string `json:"password,omitempty"` // Status holds the value of the "status" field. Status string `json:"status,omitempty"` + // Proxy expiration time (NULL means never expires). + ExpiresAt *time.Time `json:"expires_at,omitempty"` + // Fallback target on expiry: none | proxy | direct. + FallbackMode string `json:"fallback_mode,omitempty"` + // Backup proxy id when fallback_mode=proxy (self-reference). + BackupProxyID *int64 `json:"backup_proxy_id,omitempty"` + // Days before expiry to flag as expiring-soon (per proxy). + ExpiryWarnDays int `json:"expiry_warn_days,omitempty"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the ProxyQuery when eager-loading is set. Edges ProxyEdges `json:"edges"` @@ -47,9 +55,11 @@ type Proxy struct { type ProxyEdges struct { // Accounts holds the value of the accounts edge. Accounts []*Account `json:"accounts,omitempty"` + // BackupProxy holds the value of the backup_proxy edge. + BackupProxy *Proxy `json:"backup_proxy,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [1]bool + loadedTypes [2]bool } // AccountsOrErr returns the Accounts value or an error if the edge @@ -61,16 +71,27 @@ func (e ProxyEdges) AccountsOrErr() ([]*Account, error) { return nil, &NotLoadedError{edge: "accounts"} } +// BackupProxyOrErr returns the BackupProxy value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e ProxyEdges) BackupProxyOrErr() (*Proxy, error) { + if e.BackupProxy != nil { + return e.BackupProxy, nil + } else if e.loadedTypes[1] { + return nil, &NotFoundError{label: proxy.Label} + } + return nil, &NotLoadedError{edge: "backup_proxy"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*Proxy) scanValues(columns []string) ([]any, error) { values := make([]any, len(columns)) for i := range columns { switch columns[i] { - case proxy.FieldID, proxy.FieldPort: + case proxy.FieldID, proxy.FieldPort, proxy.FieldBackupProxyID, proxy.FieldExpiryWarnDays: values[i] = new(sql.NullInt64) - case proxy.FieldName, proxy.FieldProtocol, proxy.FieldHost, proxy.FieldUsername, proxy.FieldPassword, proxy.FieldStatus: + case proxy.FieldName, proxy.FieldProtocol, proxy.FieldHost, proxy.FieldUsername, proxy.FieldPassword, proxy.FieldStatus, proxy.FieldFallbackMode: values[i] = new(sql.NullString) - case proxy.FieldCreatedAt, proxy.FieldUpdatedAt, proxy.FieldDeletedAt: + case proxy.FieldCreatedAt, proxy.FieldUpdatedAt, proxy.FieldDeletedAt, proxy.FieldExpiresAt: values[i] = new(sql.NullTime) default: values[i] = new(sql.UnknownType) @@ -156,6 +177,32 @@ func (_m *Proxy) assignValues(columns []string, values []any) error { } else if value.Valid { _m.Status = value.String } + case proxy.FieldExpiresAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field expires_at", values[i]) + } else if value.Valid { + _m.ExpiresAt = new(time.Time) + *_m.ExpiresAt = value.Time + } + case proxy.FieldFallbackMode: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field fallback_mode", values[i]) + } else if value.Valid { + _m.FallbackMode = value.String + } + case proxy.FieldBackupProxyID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field backup_proxy_id", values[i]) + } else if value.Valid { + _m.BackupProxyID = new(int64) + *_m.BackupProxyID = value.Int64 + } + case proxy.FieldExpiryWarnDays: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field expiry_warn_days", values[i]) + } else if value.Valid { + _m.ExpiryWarnDays = int(value.Int64) + } default: _m.selectValues.Set(columns[i], values[i]) } @@ -174,6 +221,11 @@ func (_m *Proxy) QueryAccounts() *AccountQuery { return NewProxyClient(_m.config).QueryAccounts(_m) } +// QueryBackupProxy queries the "backup_proxy" edge of the Proxy entity. +func (_m *Proxy) QueryBackupProxy() *ProxyQuery { + return NewProxyClient(_m.config).QueryBackupProxy(_m) +} + // Update returns a builder for updating this Proxy. // Note that you need to call Proxy.Unwrap() before calling this method if this Proxy // was returned from a transaction, and the transaction was committed or rolled back. @@ -232,6 +284,22 @@ func (_m *Proxy) String() string { builder.WriteString(", ") builder.WriteString("status=") builder.WriteString(_m.Status) + builder.WriteString(", ") + if v := _m.ExpiresAt; v != nil { + builder.WriteString("expires_at=") + builder.WriteString(v.Format(time.ANSIC)) + } + builder.WriteString(", ") + builder.WriteString("fallback_mode=") + builder.WriteString(_m.FallbackMode) + builder.WriteString(", ") + if v := _m.BackupProxyID; v != nil { + builder.WriteString("backup_proxy_id=") + builder.WriteString(fmt.Sprintf("%v", *v)) + } + builder.WriteString(", ") + builder.WriteString("expiry_warn_days=") + builder.WriteString(fmt.Sprintf("%v", _m.ExpiryWarnDays)) builder.WriteByte(')') return builder.String() } diff --git a/backend/ent/proxy/proxy.go b/backend/ent/proxy/proxy.go index db7abcda32..afd45e3af5 100644 --- a/backend/ent/proxy/proxy.go +++ b/backend/ent/proxy/proxy.go @@ -35,8 +35,18 @@ const ( FieldPassword = "password" // FieldStatus holds the string denoting the status field in the database. FieldStatus = "status" + // FieldExpiresAt holds the string denoting the expires_at field in the database. + FieldExpiresAt = "expires_at" + // FieldFallbackMode holds the string denoting the fallback_mode field in the database. + FieldFallbackMode = "fallback_mode" + // FieldBackupProxyID holds the string denoting the backup_proxy_id field in the database. + FieldBackupProxyID = "backup_proxy_id" + // FieldExpiryWarnDays holds the string denoting the expiry_warn_days field in the database. + FieldExpiryWarnDays = "expiry_warn_days" // EdgeAccounts holds the string denoting the accounts edge name in mutations. EdgeAccounts = "accounts" + // EdgeBackupProxy holds the string denoting the backup_proxy edge name in mutations. + EdgeBackupProxy = "backup_proxy" // Table holds the table name of the proxy in the database. Table = "proxies" // AccountsTable is the table that holds the accounts relation/edge. @@ -46,6 +56,10 @@ const ( AccountsInverseTable = "accounts" // AccountsColumn is the table column denoting the accounts relation/edge. AccountsColumn = "proxy_id" + // BackupProxyTable is the table that holds the backup_proxy relation/edge. + BackupProxyTable = "proxies" + // BackupProxyColumn is the table column denoting the backup_proxy relation/edge. + BackupProxyColumn = "backup_proxy_id" ) // Columns holds all SQL columns for proxy fields. @@ -61,6 +75,10 @@ var Columns = []string{ FieldUsername, FieldPassword, FieldStatus, + FieldExpiresAt, + FieldFallbackMode, + FieldBackupProxyID, + FieldExpiryWarnDays, } // ValidColumn reports if the column name is valid (part of the table columns). @@ -101,6 +119,12 @@ var ( DefaultStatus string // StatusValidator is a validator for the "status" field. It is called by the builders before save. StatusValidator func(string) error + // DefaultFallbackMode holds the default value on creation for the "fallback_mode" field. + DefaultFallbackMode string + // FallbackModeValidator is a validator for the "fallback_mode" field. It is called by the builders before save. + FallbackModeValidator func(string) error + // DefaultExpiryWarnDays holds the default value on creation for the "expiry_warn_days" field. + DefaultExpiryWarnDays int ) // OrderOption defines the ordering options for the Proxy queries. @@ -161,6 +185,26 @@ func ByStatus(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldStatus, opts...).ToFunc() } +// ByExpiresAt orders the results by the expires_at field. +func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiresAt, opts...).ToFunc() +} + +// ByFallbackMode orders the results by the fallback_mode field. +func ByFallbackMode(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldFallbackMode, opts...).ToFunc() +} + +// ByBackupProxyID orders the results by the backup_proxy_id field. +func ByBackupProxyID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldBackupProxyID, opts...).ToFunc() +} + +// ByExpiryWarnDays orders the results by the expiry_warn_days field. +func ByExpiryWarnDays(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldExpiryWarnDays, opts...).ToFunc() +} + // ByAccountsCount orders the results by accounts count. func ByAccountsCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { @@ -174,6 +218,13 @@ func ByAccounts(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { sqlgraph.OrderByNeighborTerms(s, newAccountsStep(), append([]sql.OrderTerm{term}, terms...)...) } } + +// ByBackupProxyField orders the results by backup_proxy field. +func ByBackupProxyField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newBackupProxyStep(), sql.OrderByField(field, opts...)) + } +} func newAccountsStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), @@ -181,3 +232,10 @@ func newAccountsStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.O2M, true, AccountsTable, AccountsColumn), ) } +func newBackupProxyStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2O, false, BackupProxyTable, BackupProxyColumn), + ) +} diff --git a/backend/ent/proxy/where.go b/backend/ent/proxy/where.go index 0a31ad7e5b..5150196a41 100644 --- a/backend/ent/proxy/where.go +++ b/backend/ent/proxy/where.go @@ -105,6 +105,26 @@ func Status(v string) predicate.Proxy { return predicate.Proxy(sql.FieldEQ(FieldStatus, v)) } +// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ. +func ExpiresAt(v time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldExpiresAt, v)) +} + +// FallbackMode applies equality check predicate on the "fallback_mode" field. It's identical to FallbackModeEQ. +func FallbackMode(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldFallbackMode, v)) +} + +// BackupProxyID applies equality check predicate on the "backup_proxy_id" field. It's identical to BackupProxyIDEQ. +func BackupProxyID(v int64) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldBackupProxyID, v)) +} + +// ExpiryWarnDays applies equality check predicate on the "expiry_warn_days" field. It's identical to ExpiryWarnDaysEQ. +func ExpiryWarnDays(v int) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldExpiryWarnDays, v)) +} + // CreatedAtEQ applies the EQ predicate on the "created_at" field. func CreatedAtEQ(v time.Time) predicate.Proxy { return predicate.Proxy(sql.FieldEQ(FieldCreatedAt, v)) @@ -685,6 +705,191 @@ func StatusContainsFold(v string) predicate.Proxy { return predicate.Proxy(sql.FieldContainsFold(FieldStatus, v)) } +// ExpiresAtEQ applies the EQ predicate on the "expires_at" field. +func ExpiresAtEQ(v time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldExpiresAt, v)) +} + +// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field. +func ExpiresAtNEQ(v time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldNEQ(FieldExpiresAt, v)) +} + +// ExpiresAtIn applies the In predicate on the "expires_at" field. +func ExpiresAtIn(vs ...time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field. +func ExpiresAtNotIn(vs ...time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldNotIn(FieldExpiresAt, vs...)) +} + +// ExpiresAtGT applies the GT predicate on the "expires_at" field. +func ExpiresAtGT(v time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldGT(FieldExpiresAt, v)) +} + +// ExpiresAtGTE applies the GTE predicate on the "expires_at" field. +func ExpiresAtGTE(v time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldGTE(FieldExpiresAt, v)) +} + +// ExpiresAtLT applies the LT predicate on the "expires_at" field. +func ExpiresAtLT(v time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldLT(FieldExpiresAt, v)) +} + +// ExpiresAtLTE applies the LTE predicate on the "expires_at" field. +func ExpiresAtLTE(v time.Time) predicate.Proxy { + return predicate.Proxy(sql.FieldLTE(FieldExpiresAt, v)) +} + +// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field. +func ExpiresAtIsNil() predicate.Proxy { + return predicate.Proxy(sql.FieldIsNull(FieldExpiresAt)) +} + +// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field. +func ExpiresAtNotNil() predicate.Proxy { + return predicate.Proxy(sql.FieldNotNull(FieldExpiresAt)) +} + +// FallbackModeEQ applies the EQ predicate on the "fallback_mode" field. +func FallbackModeEQ(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldFallbackMode, v)) +} + +// FallbackModeNEQ applies the NEQ predicate on the "fallback_mode" field. +func FallbackModeNEQ(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldNEQ(FieldFallbackMode, v)) +} + +// FallbackModeIn applies the In predicate on the "fallback_mode" field. +func FallbackModeIn(vs ...string) predicate.Proxy { + return predicate.Proxy(sql.FieldIn(FieldFallbackMode, vs...)) +} + +// FallbackModeNotIn applies the NotIn predicate on the "fallback_mode" field. +func FallbackModeNotIn(vs ...string) predicate.Proxy { + return predicate.Proxy(sql.FieldNotIn(FieldFallbackMode, vs...)) +} + +// FallbackModeGT applies the GT predicate on the "fallback_mode" field. +func FallbackModeGT(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldGT(FieldFallbackMode, v)) +} + +// FallbackModeGTE applies the GTE predicate on the "fallback_mode" field. +func FallbackModeGTE(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldGTE(FieldFallbackMode, v)) +} + +// FallbackModeLT applies the LT predicate on the "fallback_mode" field. +func FallbackModeLT(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldLT(FieldFallbackMode, v)) +} + +// FallbackModeLTE applies the LTE predicate on the "fallback_mode" field. +func FallbackModeLTE(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldLTE(FieldFallbackMode, v)) +} + +// FallbackModeContains applies the Contains predicate on the "fallback_mode" field. +func FallbackModeContains(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldContains(FieldFallbackMode, v)) +} + +// FallbackModeHasPrefix applies the HasPrefix predicate on the "fallback_mode" field. +func FallbackModeHasPrefix(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldHasPrefix(FieldFallbackMode, v)) +} + +// FallbackModeHasSuffix applies the HasSuffix predicate on the "fallback_mode" field. +func FallbackModeHasSuffix(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldHasSuffix(FieldFallbackMode, v)) +} + +// FallbackModeEqualFold applies the EqualFold predicate on the "fallback_mode" field. +func FallbackModeEqualFold(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldEqualFold(FieldFallbackMode, v)) +} + +// FallbackModeContainsFold applies the ContainsFold predicate on the "fallback_mode" field. +func FallbackModeContainsFold(v string) predicate.Proxy { + return predicate.Proxy(sql.FieldContainsFold(FieldFallbackMode, v)) +} + +// BackupProxyIDEQ applies the EQ predicate on the "backup_proxy_id" field. +func BackupProxyIDEQ(v int64) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldBackupProxyID, v)) +} + +// BackupProxyIDNEQ applies the NEQ predicate on the "backup_proxy_id" field. +func BackupProxyIDNEQ(v int64) predicate.Proxy { + return predicate.Proxy(sql.FieldNEQ(FieldBackupProxyID, v)) +} + +// BackupProxyIDIn applies the In predicate on the "backup_proxy_id" field. +func BackupProxyIDIn(vs ...int64) predicate.Proxy { + return predicate.Proxy(sql.FieldIn(FieldBackupProxyID, vs...)) +} + +// BackupProxyIDNotIn applies the NotIn predicate on the "backup_proxy_id" field. +func BackupProxyIDNotIn(vs ...int64) predicate.Proxy { + return predicate.Proxy(sql.FieldNotIn(FieldBackupProxyID, vs...)) +} + +// BackupProxyIDIsNil applies the IsNil predicate on the "backup_proxy_id" field. +func BackupProxyIDIsNil() predicate.Proxy { + return predicate.Proxy(sql.FieldIsNull(FieldBackupProxyID)) +} + +// BackupProxyIDNotNil applies the NotNil predicate on the "backup_proxy_id" field. +func BackupProxyIDNotNil() predicate.Proxy { + return predicate.Proxy(sql.FieldNotNull(FieldBackupProxyID)) +} + +// ExpiryWarnDaysEQ applies the EQ predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysEQ(v int) predicate.Proxy { + return predicate.Proxy(sql.FieldEQ(FieldExpiryWarnDays, v)) +} + +// ExpiryWarnDaysNEQ applies the NEQ predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysNEQ(v int) predicate.Proxy { + return predicate.Proxy(sql.FieldNEQ(FieldExpiryWarnDays, v)) +} + +// ExpiryWarnDaysIn applies the In predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysIn(vs ...int) predicate.Proxy { + return predicate.Proxy(sql.FieldIn(FieldExpiryWarnDays, vs...)) +} + +// ExpiryWarnDaysNotIn applies the NotIn predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysNotIn(vs ...int) predicate.Proxy { + return predicate.Proxy(sql.FieldNotIn(FieldExpiryWarnDays, vs...)) +} + +// ExpiryWarnDaysGT applies the GT predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysGT(v int) predicate.Proxy { + return predicate.Proxy(sql.FieldGT(FieldExpiryWarnDays, v)) +} + +// ExpiryWarnDaysGTE applies the GTE predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysGTE(v int) predicate.Proxy { + return predicate.Proxy(sql.FieldGTE(FieldExpiryWarnDays, v)) +} + +// ExpiryWarnDaysLT applies the LT predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysLT(v int) predicate.Proxy { + return predicate.Proxy(sql.FieldLT(FieldExpiryWarnDays, v)) +} + +// ExpiryWarnDaysLTE applies the LTE predicate on the "expiry_warn_days" field. +func ExpiryWarnDaysLTE(v int) predicate.Proxy { + return predicate.Proxy(sql.FieldLTE(FieldExpiryWarnDays, v)) +} + // HasAccounts applies the HasEdge predicate on the "accounts" edge. func HasAccounts() predicate.Proxy { return predicate.Proxy(func(s *sql.Selector) { @@ -708,6 +913,29 @@ func HasAccountsWith(preds ...predicate.Account) predicate.Proxy { }) } +// HasBackupProxy applies the HasEdge predicate on the "backup_proxy" edge. +func HasBackupProxy() predicate.Proxy { + return predicate.Proxy(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2O, false, BackupProxyTable, BackupProxyColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasBackupProxyWith applies the HasEdge predicate on the "backup_proxy" edge with a given conditions (other predicates). +func HasBackupProxyWith(preds ...predicate.Proxy) predicate.Proxy { + return predicate.Proxy(func(s *sql.Selector) { + step := newBackupProxyStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.Proxy) predicate.Proxy { return predicate.Proxy(sql.AndPredicates(predicates...)) diff --git a/backend/ent/proxy_create.go b/backend/ent/proxy_create.go index 9687aaa260..925ce9cb31 100644 --- a/backend/ent/proxy_create.go +++ b/backend/ent/proxy_create.go @@ -131,6 +131,62 @@ func (_c *ProxyCreate) SetNillableStatus(v *string) *ProxyCreate { return _c } +// SetExpiresAt sets the "expires_at" field. +func (_c *ProxyCreate) SetExpiresAt(v time.Time) *ProxyCreate { + _c.mutation.SetExpiresAt(v) + return _c +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_c *ProxyCreate) SetNillableExpiresAt(v *time.Time) *ProxyCreate { + if v != nil { + _c.SetExpiresAt(*v) + } + return _c +} + +// SetFallbackMode sets the "fallback_mode" field. +func (_c *ProxyCreate) SetFallbackMode(v string) *ProxyCreate { + _c.mutation.SetFallbackMode(v) + return _c +} + +// SetNillableFallbackMode sets the "fallback_mode" field if the given value is not nil. +func (_c *ProxyCreate) SetNillableFallbackMode(v *string) *ProxyCreate { + if v != nil { + _c.SetFallbackMode(*v) + } + return _c +} + +// SetBackupProxyID sets the "backup_proxy_id" field. +func (_c *ProxyCreate) SetBackupProxyID(v int64) *ProxyCreate { + _c.mutation.SetBackupProxyID(v) + return _c +} + +// SetNillableBackupProxyID sets the "backup_proxy_id" field if the given value is not nil. +func (_c *ProxyCreate) SetNillableBackupProxyID(v *int64) *ProxyCreate { + if v != nil { + _c.SetBackupProxyID(*v) + } + return _c +} + +// SetExpiryWarnDays sets the "expiry_warn_days" field. +func (_c *ProxyCreate) SetExpiryWarnDays(v int) *ProxyCreate { + _c.mutation.SetExpiryWarnDays(v) + return _c +} + +// SetNillableExpiryWarnDays sets the "expiry_warn_days" field if the given value is not nil. +func (_c *ProxyCreate) SetNillableExpiryWarnDays(v *int) *ProxyCreate { + if v != nil { + _c.SetExpiryWarnDays(*v) + } + return _c +} + // AddAccountIDs adds the "accounts" edge to the Account entity by IDs. func (_c *ProxyCreate) AddAccountIDs(ids ...int64) *ProxyCreate { _c.mutation.AddAccountIDs(ids...) @@ -146,6 +202,11 @@ func (_c *ProxyCreate) AddAccounts(v ...*Account) *ProxyCreate { return _c.AddAccountIDs(ids...) } +// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity. +func (_c *ProxyCreate) SetBackupProxy(v *Proxy) *ProxyCreate { + return _c.SetBackupProxyID(v.ID) +} + // Mutation returns the ProxyMutation object of the builder. func (_c *ProxyCreate) Mutation() *ProxyMutation { return _c.mutation @@ -201,6 +262,14 @@ func (_c *ProxyCreate) defaults() error { v := proxy.DefaultStatus _c.mutation.SetStatus(v) } + if _, ok := _c.mutation.FallbackMode(); !ok { + v := proxy.DefaultFallbackMode + _c.mutation.SetFallbackMode(v) + } + if _, ok := _c.mutation.ExpiryWarnDays(); !ok { + v := proxy.DefaultExpiryWarnDays + _c.mutation.SetExpiryWarnDays(v) + } return nil } @@ -257,6 +326,17 @@ func (_c *ProxyCreate) check() error { return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Proxy.status": %w`, err)} } } + if _, ok := _c.mutation.FallbackMode(); !ok { + return &ValidationError{Name: "fallback_mode", err: errors.New(`ent: missing required field "Proxy.fallback_mode"`)} + } + if v, ok := _c.mutation.FallbackMode(); ok { + if err := proxy.FallbackModeValidator(v); err != nil { + return &ValidationError{Name: "fallback_mode", err: fmt.Errorf(`ent: validator failed for field "Proxy.fallback_mode": %w`, err)} + } + } + if _, ok := _c.mutation.ExpiryWarnDays(); !ok { + return &ValidationError{Name: "expiry_warn_days", err: errors.New(`ent: missing required field "Proxy.expiry_warn_days"`)} + } return nil } @@ -324,6 +404,18 @@ func (_c *ProxyCreate) createSpec() (*Proxy, *sqlgraph.CreateSpec) { _spec.SetField(proxy.FieldStatus, field.TypeString, value) _node.Status = value } + if value, ok := _c.mutation.ExpiresAt(); ok { + _spec.SetField(proxy.FieldExpiresAt, field.TypeTime, value) + _node.ExpiresAt = &value + } + if value, ok := _c.mutation.FallbackMode(); ok { + _spec.SetField(proxy.FieldFallbackMode, field.TypeString, value) + _node.FallbackMode = value + } + if value, ok := _c.mutation.ExpiryWarnDays(); ok { + _spec.SetField(proxy.FieldExpiryWarnDays, field.TypeInt, value) + _node.ExpiryWarnDays = value + } if nodes := _c.mutation.AccountsIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -340,6 +432,23 @@ func (_c *ProxyCreate) createSpec() (*Proxy, *sqlgraph.CreateSpec) { } _spec.Edges = append(_spec.Edges, edge) } + if nodes := _c.mutation.BackupProxyIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2O, + Inverse: false, + Table: proxy.BackupProxyTable, + Columns: []string{proxy.BackupProxyColumn}, + Bidi: true, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.BackupProxyID = &nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } @@ -524,6 +633,72 @@ func (u *ProxyUpsert) UpdateStatus() *ProxyUpsert { return u } +// SetExpiresAt sets the "expires_at" field. +func (u *ProxyUpsert) SetExpiresAt(v time.Time) *ProxyUpsert { + u.Set(proxy.FieldExpiresAt, v) + return u +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *ProxyUpsert) UpdateExpiresAt() *ProxyUpsert { + u.SetExcluded(proxy.FieldExpiresAt) + return u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *ProxyUpsert) ClearExpiresAt() *ProxyUpsert { + u.SetNull(proxy.FieldExpiresAt) + return u +} + +// SetFallbackMode sets the "fallback_mode" field. +func (u *ProxyUpsert) SetFallbackMode(v string) *ProxyUpsert { + u.Set(proxy.FieldFallbackMode, v) + return u +} + +// UpdateFallbackMode sets the "fallback_mode" field to the value that was provided on create. +func (u *ProxyUpsert) UpdateFallbackMode() *ProxyUpsert { + u.SetExcluded(proxy.FieldFallbackMode) + return u +} + +// SetBackupProxyID sets the "backup_proxy_id" field. +func (u *ProxyUpsert) SetBackupProxyID(v int64) *ProxyUpsert { + u.Set(proxy.FieldBackupProxyID, v) + return u +} + +// UpdateBackupProxyID sets the "backup_proxy_id" field to the value that was provided on create. +func (u *ProxyUpsert) UpdateBackupProxyID() *ProxyUpsert { + u.SetExcluded(proxy.FieldBackupProxyID) + return u +} + +// ClearBackupProxyID clears the value of the "backup_proxy_id" field. +func (u *ProxyUpsert) ClearBackupProxyID() *ProxyUpsert { + u.SetNull(proxy.FieldBackupProxyID) + return u +} + +// SetExpiryWarnDays sets the "expiry_warn_days" field. +func (u *ProxyUpsert) SetExpiryWarnDays(v int) *ProxyUpsert { + u.Set(proxy.FieldExpiryWarnDays, v) + return u +} + +// UpdateExpiryWarnDays sets the "expiry_warn_days" field to the value that was provided on create. +func (u *ProxyUpsert) UpdateExpiryWarnDays() *ProxyUpsert { + u.SetExcluded(proxy.FieldExpiryWarnDays) + return u +} + +// AddExpiryWarnDays adds v to the "expiry_warn_days" field. +func (u *ProxyUpsert) AddExpiryWarnDays(v int) *ProxyUpsert { + u.Add(proxy.FieldExpiryWarnDays, v) + return u +} + // UpdateNewValues updates the mutable fields using the new values that were set on create. // Using this option is equivalent to using: // @@ -723,6 +898,83 @@ func (u *ProxyUpsertOne) UpdateStatus() *ProxyUpsertOne { }) } +// SetExpiresAt sets the "expires_at" field. +func (u *ProxyUpsertOne) SetExpiresAt(v time.Time) *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *ProxyUpsertOne) UpdateExpiresAt() *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *ProxyUpsertOne) ClearExpiresAt() *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.ClearExpiresAt() + }) +} + +// SetFallbackMode sets the "fallback_mode" field. +func (u *ProxyUpsertOne) SetFallbackMode(v string) *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.SetFallbackMode(v) + }) +} + +// UpdateFallbackMode sets the "fallback_mode" field to the value that was provided on create. +func (u *ProxyUpsertOne) UpdateFallbackMode() *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.UpdateFallbackMode() + }) +} + +// SetBackupProxyID sets the "backup_proxy_id" field. +func (u *ProxyUpsertOne) SetBackupProxyID(v int64) *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.SetBackupProxyID(v) + }) +} + +// UpdateBackupProxyID sets the "backup_proxy_id" field to the value that was provided on create. +func (u *ProxyUpsertOne) UpdateBackupProxyID() *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.UpdateBackupProxyID() + }) +} + +// ClearBackupProxyID clears the value of the "backup_proxy_id" field. +func (u *ProxyUpsertOne) ClearBackupProxyID() *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.ClearBackupProxyID() + }) +} + +// SetExpiryWarnDays sets the "expiry_warn_days" field. +func (u *ProxyUpsertOne) SetExpiryWarnDays(v int) *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.SetExpiryWarnDays(v) + }) +} + +// AddExpiryWarnDays adds v to the "expiry_warn_days" field. +func (u *ProxyUpsertOne) AddExpiryWarnDays(v int) *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.AddExpiryWarnDays(v) + }) +} + +// UpdateExpiryWarnDays sets the "expiry_warn_days" field to the value that was provided on create. +func (u *ProxyUpsertOne) UpdateExpiryWarnDays() *ProxyUpsertOne { + return u.Update(func(s *ProxyUpsert) { + s.UpdateExpiryWarnDays() + }) +} + // Exec executes the query. func (u *ProxyUpsertOne) Exec(ctx context.Context) error { if len(u.create.conflict) == 0 { @@ -1088,6 +1340,83 @@ func (u *ProxyUpsertBulk) UpdateStatus() *ProxyUpsertBulk { }) } +// SetExpiresAt sets the "expires_at" field. +func (u *ProxyUpsertBulk) SetExpiresAt(v time.Time) *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.SetExpiresAt(v) + }) +} + +// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create. +func (u *ProxyUpsertBulk) UpdateExpiresAt() *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.UpdateExpiresAt() + }) +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (u *ProxyUpsertBulk) ClearExpiresAt() *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.ClearExpiresAt() + }) +} + +// SetFallbackMode sets the "fallback_mode" field. +func (u *ProxyUpsertBulk) SetFallbackMode(v string) *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.SetFallbackMode(v) + }) +} + +// UpdateFallbackMode sets the "fallback_mode" field to the value that was provided on create. +func (u *ProxyUpsertBulk) UpdateFallbackMode() *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.UpdateFallbackMode() + }) +} + +// SetBackupProxyID sets the "backup_proxy_id" field. +func (u *ProxyUpsertBulk) SetBackupProxyID(v int64) *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.SetBackupProxyID(v) + }) +} + +// UpdateBackupProxyID sets the "backup_proxy_id" field to the value that was provided on create. +func (u *ProxyUpsertBulk) UpdateBackupProxyID() *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.UpdateBackupProxyID() + }) +} + +// ClearBackupProxyID clears the value of the "backup_proxy_id" field. +func (u *ProxyUpsertBulk) ClearBackupProxyID() *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.ClearBackupProxyID() + }) +} + +// SetExpiryWarnDays sets the "expiry_warn_days" field. +func (u *ProxyUpsertBulk) SetExpiryWarnDays(v int) *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.SetExpiryWarnDays(v) + }) +} + +// AddExpiryWarnDays adds v to the "expiry_warn_days" field. +func (u *ProxyUpsertBulk) AddExpiryWarnDays(v int) *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.AddExpiryWarnDays(v) + }) +} + +// UpdateExpiryWarnDays sets the "expiry_warn_days" field to the value that was provided on create. +func (u *ProxyUpsertBulk) UpdateExpiryWarnDays() *ProxyUpsertBulk { + return u.Update(func(s *ProxyUpsert) { + s.UpdateExpiryWarnDays() + }) +} + // Exec executes the query. func (u *ProxyUpsertBulk) Exec(ctx context.Context) error { if u.create.err != nil { diff --git a/backend/ent/proxy_query.go b/backend/ent/proxy_query.go index b817d139d4..93caf34665 100644 --- a/backend/ent/proxy_query.go +++ b/backend/ent/proxy_query.go @@ -21,12 +21,13 @@ import ( // ProxyQuery is the builder for querying Proxy entities. type ProxyQuery struct { config - ctx *QueryContext - order []proxy.OrderOption - inters []Interceptor - predicates []predicate.Proxy - withAccounts *AccountQuery - modifiers []func(*sql.Selector) + ctx *QueryContext + order []proxy.OrderOption + inters []Interceptor + predicates []predicate.Proxy + withAccounts *AccountQuery + withBackupProxy *ProxyQuery + modifiers []func(*sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) @@ -85,6 +86,28 @@ func (_q *ProxyQuery) QueryAccounts() *AccountQuery { return query } +// QueryBackupProxy chains the current query on the "backup_proxy" edge. +func (_q *ProxyQuery) QueryBackupProxy() *ProxyQuery { + query := (&ProxyClient{config: _q.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + selector := _q.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(proxy.Table, proxy.FieldID, selector), + sqlgraph.To(proxy.Table, proxy.FieldID), + sqlgraph.Edge(sqlgraph.O2O, false, proxy.BackupProxyTable, proxy.BackupProxyColumn), + ) + fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first Proxy entity from the query. // Returns a *NotFoundError when no Proxy was found. func (_q *ProxyQuery) First(ctx context.Context) (*Proxy, error) { @@ -272,12 +295,13 @@ func (_q *ProxyQuery) Clone() *ProxyQuery { return nil } return &ProxyQuery{ - config: _q.config, - ctx: _q.ctx.Clone(), - order: append([]proxy.OrderOption{}, _q.order...), - inters: append([]Interceptor{}, _q.inters...), - predicates: append([]predicate.Proxy{}, _q.predicates...), - withAccounts: _q.withAccounts.Clone(), + config: _q.config, + ctx: _q.ctx.Clone(), + order: append([]proxy.OrderOption{}, _q.order...), + inters: append([]Interceptor{}, _q.inters...), + predicates: append([]predicate.Proxy{}, _q.predicates...), + withAccounts: _q.withAccounts.Clone(), + withBackupProxy: _q.withBackupProxy.Clone(), // clone intermediate query. sql: _q.sql.Clone(), path: _q.path, @@ -295,6 +319,17 @@ func (_q *ProxyQuery) WithAccounts(opts ...func(*AccountQuery)) *ProxyQuery { return _q } +// WithBackupProxy tells the query-builder to eager-load the nodes that are connected to +// the "backup_proxy" edge. The optional arguments are used to configure the query builder of the edge. +func (_q *ProxyQuery) WithBackupProxy(opts ...func(*ProxyQuery)) *ProxyQuery { + query := (&ProxyClient{config: _q.config}).Query() + for _, opt := range opts { + opt(query) + } + _q.withBackupProxy = query + return _q +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -373,8 +408,9 @@ func (_q *ProxyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proxy, var ( nodes = []*Proxy{} _spec = _q.querySpec() - loadedTypes = [1]bool{ + loadedTypes = [2]bool{ _q.withAccounts != nil, + _q.withBackupProxy != nil, } ) _spec.ScanValues = func(columns []string) ([]any, error) { @@ -405,6 +441,12 @@ func (_q *ProxyQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Proxy, return nil, err } } + if query := _q.withBackupProxy; query != nil { + if err := _q.loadBackupProxy(ctx, query, nodes, nil, + func(n *Proxy, e *Proxy) { n.Edges.BackupProxy = e }); err != nil { + return nil, err + } + } return nodes, nil } @@ -441,6 +483,38 @@ func (_q *ProxyQuery) loadAccounts(ctx context.Context, query *AccountQuery, nod } return nil } +func (_q *ProxyQuery) loadBackupProxy(ctx context.Context, query *ProxyQuery, nodes []*Proxy, init func(*Proxy), assign func(*Proxy, *Proxy)) error { + ids := make([]int64, 0, len(nodes)) + nodeids := make(map[int64][]*Proxy) + for i := range nodes { + if nodes[i].BackupProxyID == nil { + continue + } + fk := *nodes[i].BackupProxyID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(proxy.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "backup_proxy_id" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} func (_q *ProxyQuery) sqlCount(ctx context.Context) (int, error) { _spec := _q.querySpec() @@ -470,6 +544,9 @@ func (_q *ProxyQuery) querySpec() *sqlgraph.QuerySpec { _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) } } + if _q.withBackupProxy != nil { + _spec.Node.AddColumnOnce(proxy.FieldBackupProxyID) + } } if ps := _q.predicates; len(ps) > 0 { _spec.Predicate = func(selector *sql.Selector) { diff --git a/backend/ent/proxy_update.go b/backend/ent/proxy_update.go index d487857f82..36b49ced6d 100644 --- a/backend/ent/proxy_update.go +++ b/backend/ent/proxy_update.go @@ -172,6 +172,81 @@ func (_u *ProxyUpdate) SetNillableStatus(v *string) *ProxyUpdate { return _u } +// SetExpiresAt sets the "expires_at" field. +func (_u *ProxyUpdate) SetExpiresAt(v time.Time) *ProxyUpdate { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *ProxyUpdate) SetNillableExpiresAt(v *time.Time) *ProxyUpdate { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *ProxyUpdate) ClearExpiresAt() *ProxyUpdate { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetFallbackMode sets the "fallback_mode" field. +func (_u *ProxyUpdate) SetFallbackMode(v string) *ProxyUpdate { + _u.mutation.SetFallbackMode(v) + return _u +} + +// SetNillableFallbackMode sets the "fallback_mode" field if the given value is not nil. +func (_u *ProxyUpdate) SetNillableFallbackMode(v *string) *ProxyUpdate { + if v != nil { + _u.SetFallbackMode(*v) + } + return _u +} + +// SetBackupProxyID sets the "backup_proxy_id" field. +func (_u *ProxyUpdate) SetBackupProxyID(v int64) *ProxyUpdate { + _u.mutation.SetBackupProxyID(v) + return _u +} + +// SetNillableBackupProxyID sets the "backup_proxy_id" field if the given value is not nil. +func (_u *ProxyUpdate) SetNillableBackupProxyID(v *int64) *ProxyUpdate { + if v != nil { + _u.SetBackupProxyID(*v) + } + return _u +} + +// ClearBackupProxyID clears the value of the "backup_proxy_id" field. +func (_u *ProxyUpdate) ClearBackupProxyID() *ProxyUpdate { + _u.mutation.ClearBackupProxyID() + return _u +} + +// SetExpiryWarnDays sets the "expiry_warn_days" field. +func (_u *ProxyUpdate) SetExpiryWarnDays(v int) *ProxyUpdate { + _u.mutation.ResetExpiryWarnDays() + _u.mutation.SetExpiryWarnDays(v) + return _u +} + +// SetNillableExpiryWarnDays sets the "expiry_warn_days" field if the given value is not nil. +func (_u *ProxyUpdate) SetNillableExpiryWarnDays(v *int) *ProxyUpdate { + if v != nil { + _u.SetExpiryWarnDays(*v) + } + return _u +} + +// AddExpiryWarnDays adds value to the "expiry_warn_days" field. +func (_u *ProxyUpdate) AddExpiryWarnDays(v int) *ProxyUpdate { + _u.mutation.AddExpiryWarnDays(v) + return _u +} + // AddAccountIDs adds the "accounts" edge to the Account entity by IDs. func (_u *ProxyUpdate) AddAccountIDs(ids ...int64) *ProxyUpdate { _u.mutation.AddAccountIDs(ids...) @@ -187,6 +262,11 @@ func (_u *ProxyUpdate) AddAccounts(v ...*Account) *ProxyUpdate { return _u.AddAccountIDs(ids...) } +// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity. +func (_u *ProxyUpdate) SetBackupProxy(v *Proxy) *ProxyUpdate { + return _u.SetBackupProxyID(v.ID) +} + // Mutation returns the ProxyMutation object of the builder. func (_u *ProxyUpdate) Mutation() *ProxyMutation { return _u.mutation @@ -213,6 +293,12 @@ func (_u *ProxyUpdate) RemoveAccounts(v ...*Account) *ProxyUpdate { return _u.RemoveAccountIDs(ids...) } +// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity. +func (_u *ProxyUpdate) ClearBackupProxy() *ProxyUpdate { + _u.mutation.ClearBackupProxy() + return _u +} + // Save executes the query and returns the number of nodes affected by the update operation. func (_u *ProxyUpdate) Save(ctx context.Context) (int, error) { if err := _u.defaults(); err != nil { @@ -287,6 +373,11 @@ func (_u *ProxyUpdate) check() error { return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Proxy.status": %w`, err)} } } + if v, ok := _u.mutation.FallbackMode(); ok { + if err := proxy.FallbackModeValidator(v); err != nil { + return &ValidationError{Name: "fallback_mode", err: fmt.Errorf(`ent: validator failed for field "Proxy.fallback_mode": %w`, err)} + } + } return nil } @@ -341,6 +432,21 @@ func (_u *ProxyUpdate) sqlSave(ctx context.Context) (_node int, err error) { if value, ok := _u.mutation.Status(); ok { _spec.SetField(proxy.FieldStatus, field.TypeString, value) } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(proxy.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(proxy.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.FallbackMode(); ok { + _spec.SetField(proxy.FieldFallbackMode, field.TypeString, value) + } + if value, ok := _u.mutation.ExpiryWarnDays(); ok { + _spec.SetField(proxy.FieldExpiryWarnDays, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedExpiryWarnDays(); ok { + _spec.AddField(proxy.FieldExpiryWarnDays, field.TypeInt, value) + } if _u.mutation.AccountsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -386,6 +492,35 @@ func (_u *ProxyUpdate) sqlSave(ctx context.Context) (_node int, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.BackupProxyCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2O, + Inverse: false, + Table: proxy.BackupProxyTable, + Columns: []string{proxy.BackupProxyColumn}, + Bidi: true, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.BackupProxyIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2O, + Inverse: false, + Table: proxy.BackupProxyTable, + Columns: []string{proxy.BackupProxyColumn}, + Bidi: true, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if _node, err = sqlgraph.UpdateNodes(ctx, _u.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{proxy.Label} @@ -549,6 +684,81 @@ func (_u *ProxyUpdateOne) SetNillableStatus(v *string) *ProxyUpdateOne { return _u } +// SetExpiresAt sets the "expires_at" field. +func (_u *ProxyUpdateOne) SetExpiresAt(v time.Time) *ProxyUpdateOne { + _u.mutation.SetExpiresAt(v) + return _u +} + +// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil. +func (_u *ProxyUpdateOne) SetNillableExpiresAt(v *time.Time) *ProxyUpdateOne { + if v != nil { + _u.SetExpiresAt(*v) + } + return _u +} + +// ClearExpiresAt clears the value of the "expires_at" field. +func (_u *ProxyUpdateOne) ClearExpiresAt() *ProxyUpdateOne { + _u.mutation.ClearExpiresAt() + return _u +} + +// SetFallbackMode sets the "fallback_mode" field. +func (_u *ProxyUpdateOne) SetFallbackMode(v string) *ProxyUpdateOne { + _u.mutation.SetFallbackMode(v) + return _u +} + +// SetNillableFallbackMode sets the "fallback_mode" field if the given value is not nil. +func (_u *ProxyUpdateOne) SetNillableFallbackMode(v *string) *ProxyUpdateOne { + if v != nil { + _u.SetFallbackMode(*v) + } + return _u +} + +// SetBackupProxyID sets the "backup_proxy_id" field. +func (_u *ProxyUpdateOne) SetBackupProxyID(v int64) *ProxyUpdateOne { + _u.mutation.SetBackupProxyID(v) + return _u +} + +// SetNillableBackupProxyID sets the "backup_proxy_id" field if the given value is not nil. +func (_u *ProxyUpdateOne) SetNillableBackupProxyID(v *int64) *ProxyUpdateOne { + if v != nil { + _u.SetBackupProxyID(*v) + } + return _u +} + +// ClearBackupProxyID clears the value of the "backup_proxy_id" field. +func (_u *ProxyUpdateOne) ClearBackupProxyID() *ProxyUpdateOne { + _u.mutation.ClearBackupProxyID() + return _u +} + +// SetExpiryWarnDays sets the "expiry_warn_days" field. +func (_u *ProxyUpdateOne) SetExpiryWarnDays(v int) *ProxyUpdateOne { + _u.mutation.ResetExpiryWarnDays() + _u.mutation.SetExpiryWarnDays(v) + return _u +} + +// SetNillableExpiryWarnDays sets the "expiry_warn_days" field if the given value is not nil. +func (_u *ProxyUpdateOne) SetNillableExpiryWarnDays(v *int) *ProxyUpdateOne { + if v != nil { + _u.SetExpiryWarnDays(*v) + } + return _u +} + +// AddExpiryWarnDays adds value to the "expiry_warn_days" field. +func (_u *ProxyUpdateOne) AddExpiryWarnDays(v int) *ProxyUpdateOne { + _u.mutation.AddExpiryWarnDays(v) + return _u +} + // AddAccountIDs adds the "accounts" edge to the Account entity by IDs. func (_u *ProxyUpdateOne) AddAccountIDs(ids ...int64) *ProxyUpdateOne { _u.mutation.AddAccountIDs(ids...) @@ -564,6 +774,11 @@ func (_u *ProxyUpdateOne) AddAccounts(v ...*Account) *ProxyUpdateOne { return _u.AddAccountIDs(ids...) } +// SetBackupProxy sets the "backup_proxy" edge to the Proxy entity. +func (_u *ProxyUpdateOne) SetBackupProxy(v *Proxy) *ProxyUpdateOne { + return _u.SetBackupProxyID(v.ID) +} + // Mutation returns the ProxyMutation object of the builder. func (_u *ProxyUpdateOne) Mutation() *ProxyMutation { return _u.mutation @@ -590,6 +805,12 @@ func (_u *ProxyUpdateOne) RemoveAccounts(v ...*Account) *ProxyUpdateOne { return _u.RemoveAccountIDs(ids...) } +// ClearBackupProxy clears the "backup_proxy" edge to the Proxy entity. +func (_u *ProxyUpdateOne) ClearBackupProxy() *ProxyUpdateOne { + _u.mutation.ClearBackupProxy() + return _u +} + // Where appends a list predicates to the ProxyUpdate builder. func (_u *ProxyUpdateOne) Where(ps ...predicate.Proxy) *ProxyUpdateOne { _u.mutation.Where(ps...) @@ -677,6 +898,11 @@ func (_u *ProxyUpdateOne) check() error { return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Proxy.status": %w`, err)} } } + if v, ok := _u.mutation.FallbackMode(); ok { + if err := proxy.FallbackModeValidator(v); err != nil { + return &ValidationError{Name: "fallback_mode", err: fmt.Errorf(`ent: validator failed for field "Proxy.fallback_mode": %w`, err)} + } + } return nil } @@ -748,6 +974,21 @@ func (_u *ProxyUpdateOne) sqlSave(ctx context.Context) (_node *Proxy, err error) if value, ok := _u.mutation.Status(); ok { _spec.SetField(proxy.FieldStatus, field.TypeString, value) } + if value, ok := _u.mutation.ExpiresAt(); ok { + _spec.SetField(proxy.FieldExpiresAt, field.TypeTime, value) + } + if _u.mutation.ExpiresAtCleared() { + _spec.ClearField(proxy.FieldExpiresAt, field.TypeTime) + } + if value, ok := _u.mutation.FallbackMode(); ok { + _spec.SetField(proxy.FieldFallbackMode, field.TypeString, value) + } + if value, ok := _u.mutation.ExpiryWarnDays(); ok { + _spec.SetField(proxy.FieldExpiryWarnDays, field.TypeInt, value) + } + if value, ok := _u.mutation.AddedExpiryWarnDays(); ok { + _spec.AddField(proxy.FieldExpiryWarnDays, field.TypeInt, value) + } if _u.mutation.AccountsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -793,6 +1034,35 @@ func (_u *ProxyUpdateOne) sqlSave(ctx context.Context) (_node *Proxy, err error) } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.BackupProxyCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2O, + Inverse: false, + Table: proxy.BackupProxyTable, + Columns: []string{proxy.BackupProxyColumn}, + Bidi: true, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.BackupProxyIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2O, + Inverse: false, + Table: proxy.BackupProxyTable, + Columns: []string{proxy.BackupProxyColumn}, + Bidi: true, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(proxy.FieldID, field.TypeInt64), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _node = &Proxy{config: _u.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index fdb837e805..09d2a85f7d 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -223,33 +223,33 @@ func init() { // account.DefaultExtra holds the default value on creation for the extra field. account.DefaultExtra = accountDescExtra.Default.(func() map[string]interface{}) // accountDescConcurrency is the schema descriptor for concurrency field. - accountDescConcurrency := accountFields[7].Descriptor() + accountDescConcurrency := accountFields[8].Descriptor() // account.DefaultConcurrency holds the default value on creation for the concurrency field. account.DefaultConcurrency = accountDescConcurrency.Default.(int) // accountDescPriority is the schema descriptor for priority field. - accountDescPriority := accountFields[9].Descriptor() + accountDescPriority := accountFields[10].Descriptor() // account.DefaultPriority holds the default value on creation for the priority field. account.DefaultPriority = accountDescPriority.Default.(int) // accountDescRateMultiplier is the schema descriptor for rate_multiplier field. - accountDescRateMultiplier := accountFields[10].Descriptor() + accountDescRateMultiplier := accountFields[11].Descriptor() // account.DefaultRateMultiplier holds the default value on creation for the rate_multiplier field. account.DefaultRateMultiplier = accountDescRateMultiplier.Default.(float64) // accountDescStatus is the schema descriptor for status field. - accountDescStatus := accountFields[11].Descriptor() + accountDescStatus := accountFields[12].Descriptor() // account.DefaultStatus holds the default value on creation for the status field. account.DefaultStatus = accountDescStatus.Default.(string) // account.StatusValidator is a validator for the "status" field. It is called by the builders before save. account.StatusValidator = accountDescStatus.Validators[0].(func(string) error) // accountDescAutoPauseOnExpired is the schema descriptor for auto_pause_on_expired field. - accountDescAutoPauseOnExpired := accountFields[15].Descriptor() + accountDescAutoPauseOnExpired := accountFields[16].Descriptor() // account.DefaultAutoPauseOnExpired holds the default value on creation for the auto_pause_on_expired field. account.DefaultAutoPauseOnExpired = accountDescAutoPauseOnExpired.Default.(bool) // accountDescSchedulable is the schema descriptor for schedulable field. - accountDescSchedulable := accountFields[16].Descriptor() + accountDescSchedulable := accountFields[17].Descriptor() // account.DefaultSchedulable holds the default value on creation for the schedulable field. account.DefaultSchedulable = accountDescSchedulable.Default.(bool) // accountDescSessionWindowStatus is the schema descriptor for session_window_status field. - accountDescSessionWindowStatus := accountFields[24].Descriptor() + accountDescSessionWindowStatus := accountFields[25].Descriptor() // account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save. account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error) accountgroupFields := schema.AccountGroup{}.Fields() @@ -1362,6 +1362,16 @@ func init() { proxy.DefaultStatus = proxyDescStatus.Default.(string) // proxy.StatusValidator is a validator for the "status" field. It is called by the builders before save. proxy.StatusValidator = proxyDescStatus.Validators[0].(func(string) error) + // proxyDescFallbackMode is the schema descriptor for fallback_mode field. + proxyDescFallbackMode := proxyFields[8].Descriptor() + // proxy.DefaultFallbackMode holds the default value on creation for the fallback_mode field. + proxy.DefaultFallbackMode = proxyDescFallbackMode.Default.(string) + // proxy.FallbackModeValidator is a validator for the "fallback_mode" field. It is called by the builders before save. + proxy.FallbackModeValidator = proxyDescFallbackMode.Validators[0].(func(string) error) + // proxyDescExpiryWarnDays is the schema descriptor for expiry_warn_days field. + proxyDescExpiryWarnDays := proxyFields[10].Descriptor() + // proxy.DefaultExpiryWarnDays holds the default value on creation for the expiry_warn_days field. + proxy.DefaultExpiryWarnDays = proxyDescExpiryWarnDays.Default.(int) redeemcodeFields := schema.RedeemCode{}.Fields() _ = redeemcodeFields // redeemcodeDescCode is the schema descriptor for code field. diff --git a/backend/ent/schema/account.go b/backend/ent/schema/account.go index 5616d39915..5892e606b0 100644 --- a/backend/ent/schema/account.go +++ b/backend/ent/schema/account.go @@ -91,6 +91,9 @@ func (Account) Fields() []ent.Field { field.Int64("proxy_id"). Optional(). Nillable(), + field.Int64("proxy_fallback_origin_id"). + Optional().Nillable(). + Comment("Original proxy id replaced by expiry-fallback; for manual revert. NULL = not in fallback."), // concurrency: 账户最大并发请求数 // 用于限制同一时间对该账户发起的请求数量 diff --git a/backend/ent/schema/proxy.go b/backend/ent/schema/proxy.go index 46d657d36d..f544811c5c 100644 --- a/backend/ent/schema/proxy.go +++ b/backend/ent/schema/proxy.go @@ -52,6 +52,18 @@ func (Proxy) Fields() []ent.Field { field.String("status"). MaxLen(20). Default("active"), + field.Time("expires_at"). + Optional().Nillable(). + Comment("Proxy expiration time (NULL means never expires)."), + field.String("fallback_mode"). + MaxLen(20).Default("none"). + Comment("Fallback target on expiry: none | proxy | direct."), + field.Int64("backup_proxy_id"). + Optional().Nillable(). + Comment("Backup proxy id when fallback_mode=proxy (self-reference)."), + field.Int("expiry_warn_days"). + Default(7). + Comment("Days before expiry to flag as expiring-soon (per proxy)."), } } @@ -61,6 +73,9 @@ func (Proxy) Edges() []ent.Edge { // accounts: 使用此代理的账户(反向边) edge.From("accounts", Account.Type). Ref("proxy"), + edge.To("backup_proxy", Proxy.Type). + Field("backup_proxy_id"). + Unique(), } } @@ -68,5 +83,7 @@ func (Proxy) Indexes() []ent.Index { return []ent.Index{ index.Fields("status"), index.Fields("deleted_at"), + index.Fields("expires_at"), + index.Fields("backup_proxy_id"), } } diff --git a/backend/internal/handler/admin/account_data.go b/backend/internal/handler/admin/account_data.go index 50beadf68e..94c4f45f7d 100644 --- a/backend/internal/handler/admin/account_data.go +++ b/backend/internal/handler/admin/account_data.go @@ -33,14 +33,18 @@ type DataPayload struct { } type DataProxy struct { - ProxyKey string `json:"proxy_key"` - Name string `json:"name"` - Protocol string `json:"protocol"` - Host string `json:"host"` - Port int `json:"port"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Status string `json:"status"` + ProxyKey string `json:"proxy_key"` + Name string `json:"name"` + Protocol string `json:"protocol"` + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Status string `json:"status"` + ExpiresAt *int64 `json:"expires_at,omitempty"` // unix 秒,与 DataAccount.ExpiresAt 风格一致 + FallbackMode string `json:"fallback_mode,omitempty"` // none/direct/proxy + BackupProxyName string `json:"backup_proxy_name,omitempty"` // 备用代理 name(跨实例按 name 反查) + ExpiryWarnDays int `json:"expiry_warn_days,omitempty"` } // DataAccount 是管理员显式备份导出使用的账号结构,故意不走 dto.Account 的脱敏路径, @@ -118,21 +122,41 @@ func (h *AccountHandler) ExportData(c *gin.Context) { proxies = []service.Proxy{} } + // 构建 id→name 映射,用于导出备用代理 name + proxyNameByID := make(map[int64]string, len(proxies)) + for i := range proxies { + proxyNameByID[proxies[i].ID] = proxies[i].Name + } + proxyKeyByID := make(map[int64]string, len(proxies)) dataProxies := make([]DataProxy, 0, len(proxies)) for i := range proxies { p := proxies[i] key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password) proxyKeyByID[p.ID] = key + + var expiresAt *int64 + if p.ExpiresAt != nil { + v := p.ExpiresAt.Unix() + expiresAt = &v + } + var backupProxyName string + if p.BackupProxyID != nil { + backupProxyName = proxyNameByID[*p.BackupProxyID] + } dataProxies = append(dataProxies, DataProxy{ - ProxyKey: key, - Name: p.Name, - Protocol: p.Protocol, - Host: p.Host, - Port: p.Port, - Username: p.Username, - Password: p.Password, - Status: p.Status, + ProxyKey: key, + Name: p.Name, + Protocol: p.Protocol, + Host: p.Host, + Port: p.Port, + Username: p.Username, + Password: p.Password, + Status: p.Status, + ExpiresAt: expiresAt, + FallbackMode: p.FallbackMode, + BackupProxyName: backupProxyName, + ExpiryWarnDays: p.ExpiryWarnDays, }) } @@ -207,10 +231,15 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest) } proxyKeyToID := make(map[string]int64, len(existingProxies)) + // proxyNameToID 用于 backup_proxy_name 反查:DB 已有 + 本批次新建均会写入 + proxyNameToID := make(map[string]int64, len(existingProxies)) for i := range existingProxies { p := existingProxies[i] key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password) proxyKeyToID[key] = p.ID + if p.Name != "" { + proxyNameToID[p.Name] = p.ID + } } for i := range dataPayload.Proxies { @@ -235,21 +264,76 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest) result.ProxyReused++ if normalizedStatus != "" { if proxy, getErr := h.adminService.GetProxy(ctx, existingID); getErr == nil && proxy != nil && proxy.Status != normalizedStatus { + // 同步 status 时传入完整字段,避免零值覆盖已存在代理的有效期/fallback 配置。 + var existingExpiresAt *time.Time + if item.ExpiresAt != nil { + t := time.Unix(*item.ExpiresAt, 0).UTC() + existingExpiresAt = &t + } + existingFallbackMode := item.FallbackMode + if existingFallbackMode == "" { + existingFallbackMode = service.FallbackModeNone + } + var existingBackupProxyID *int64 + if item.BackupProxyName != "" { + if bid, ok := proxyNameToID[item.BackupProxyName]; ok { + existingBackupProxyID = &bid + } + } _, _ = h.adminService.UpdateProxy(ctx, existingID, &service.UpdateProxyInput{ - Status: normalizedStatus, + Status: normalizedStatus, + ExpiresAt: existingExpiresAt, + FallbackMode: existingFallbackMode, + BackupProxyID: existingBackupProxyID, + ExpiryWarnDays: item.ExpiryWarnDays, + Name: proxy.Name, + Protocol: proxy.Protocol, + Host: proxy.Host, + Port: proxy.Port, + Username: proxy.Username, + Password: proxy.Password, }) } } continue } + // 解析 expires_at(unix 秒 → *time.Time) + var expiresAt *time.Time + if item.ExpiresAt != nil { + t := time.Unix(*item.ExpiresAt, 0).UTC() + expiresAt = &t + } + + // 解析 backup_proxy_name → backup_proxy_id + fallbackMode := item.FallbackMode + var backupProxyID *int64 + if item.BackupProxyName != "" { + if bid, ok := proxyNameToID[item.BackupProxyName]; ok { + backupProxyID = &bid + } else { + // 查不到备用代理:降级 fallback_mode=none,记录 warning + fallbackMode = service.FallbackModeNone + result.Errors = append(result.Errors, DataImportError{ + Kind: "proxy", + Name: item.Name, + ProxyKey: key, + Message: fmt.Sprintf("backup_proxy_name %q not found, fallback_mode downgraded to none", item.BackupProxyName), + }) + } + } + created, createErr := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{ - Name: defaultProxyName(item.Name), - Protocol: item.Protocol, - Host: item.Host, - Port: item.Port, - Username: item.Username, - Password: item.Password, + Name: defaultProxyName(item.Name), + Protocol: item.Protocol, + Host: item.Host, + Port: item.Port, + Username: item.Username, + Password: item.Password, + ExpiresAt: expiresAt, + FallbackMode: fallbackMode, + BackupProxyID: backupProxyID, + ExpiryWarnDays: item.ExpiryWarnDays, }) if createErr != nil { result.ProxyFailed++ @@ -262,11 +346,26 @@ func (h *AccountHandler) importData(ctx context.Context, req DataImportRequest) continue } proxyKeyToID[key] = created.ID + // 把新建代理的 name 也加入反查表,供后续批内代理引用 + if created.Name != "" { + proxyNameToID[created.Name] = created.ID + } result.ProxyCreated++ if normalizedStatus != "" && normalizedStatus != created.Status { + // 新建后同步 status 时,传入完整字段,避免零值覆盖刚创建的有效期/fallback 配置。 _, _ = h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{ - Status: normalizedStatus, + Status: normalizedStatus, + ExpiresAt: expiresAt, + FallbackMode: fallbackMode, + BackupProxyID: backupProxyID, + ExpiryWarnDays: item.ExpiryWarnDays, + Name: created.Name, + Protocol: created.Protocol, + Host: created.Host, + Port: created.Port, + Username: created.Username, + Password: created.Password, }) } } @@ -646,6 +745,9 @@ func normalizeProxyStatus(status string) string { return service.StatusActive case "inactive", service.StatusDisabled: return "inactive" + case "expired": + // 导入 expired 代理按 inactive 处理,避免导入即触发到期改投逻辑 + return "inactive" default: return normalized } diff --git a/backend/internal/handler/admin/account_handler.go b/backend/internal/handler/admin/account_handler.go index 5719534230..65ad5804fc 100644 --- a/backend/internal/handler/admin/account_handler.go +++ b/backend/internal/handler/admin/account_handler.go @@ -1132,6 +1132,21 @@ func (h *AccountHandler) ClearError(c *gin.Context) { response.Success(c, h.buildAccountResponseWithRuntime(c.Request.Context(), account)) } +// RevertProxyFallback handles reverting account proxy to original before fallback. +// POST /api/v1/admin/accounts/:id/revert-proxy-fallback +func (h *AccountHandler) RevertProxyFallback(c *gin.Context) { + id, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + response.BadRequest(c, "Invalid account ID") + return + } + if err := h.adminService.RevertAccountProxyFallback(c.Request.Context(), id); err != nil { + response.ErrorFrom(c, err) + return + } + response.Success(c, gin.H{"message": "reverted"}) +} + // BatchClearError handles batch clearing account errors // POST /api/v1/admin/accounts/batch-clear-error func (h *AccountHandler) BatchClearError(c *gin.Context) { diff --git a/backend/internal/handler/admin/admin_service_stub_test.go b/backend/internal/handler/admin/admin_service_stub_test.go index 819f0cdc02..bc4da86c52 100644 --- a/backend/internal/handler/admin/admin_service_stub_test.go +++ b/backend/internal/handler/admin/admin_service_stub_test.go @@ -628,5 +628,9 @@ func (s *stubAdminService) ReplaceUserGroup(ctx context.Context, userID, oldGrou return &service.ReplaceUserGroupResult{MigratedKeys: 0}, nil } +func (s *stubAdminService) RevertAccountProxyFallback(ctx context.Context, id int64) error { + return nil +} + // Ensure stub implements interface. var _ service.AdminService = (*stubAdminService)(nil) diff --git a/backend/internal/handler/admin/ops_alerts_handler.go b/backend/internal/handler/admin/ops_alerts_handler.go index 6356a0bbea..3b8c7f00b6 100644 --- a/backend/internal/handler/admin/ops_alerts_handler.go +++ b/backend/internal/handler/admin/ops_alerts_handler.go @@ -31,6 +31,8 @@ var validOpsAlertMetricTypes = []string{ "account_error_ratio", "account_temp_unscheduled_count", "overload_account_count", + "proxy_expired_count", + "proxy_expiring_soon_count", } var validOpsAlertMetricTypeSet = func() map[string]struct{} { diff --git a/backend/internal/handler/admin/proxy_data.go b/backend/internal/handler/admin/proxy_data.go index 8149ce3b3c..778208c68c 100644 --- a/backend/internal/handler/admin/proxy_data.go +++ b/backend/internal/handler/admin/proxy_data.go @@ -46,19 +46,39 @@ func (h *ProxyHandler) ExportData(c *gin.Context) { } } + // 构建 id→name 映射,用于导出备用代理 name + proxyNameByID := make(map[int64]string, len(proxies)) + for i := range proxies { + proxyNameByID[proxies[i].ID] = proxies[i].Name + } + dataProxies := make([]DataProxy, 0, len(proxies)) for i := range proxies { p := proxies[i] key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password) + + var expiresAt *int64 + if p.ExpiresAt != nil { + v := p.ExpiresAt.Unix() + expiresAt = &v + } + var backupProxyName string + if p.BackupProxyID != nil { + backupProxyName = proxyNameByID[*p.BackupProxyID] + } dataProxies = append(dataProxies, DataProxy{ - ProxyKey: key, - Name: p.Name, - Protocol: p.Protocol, - Host: p.Host, - Port: p.Port, - Username: p.Username, - Password: p.Password, - Status: p.Status, + ProxyKey: key, + Name: p.Name, + Protocol: p.Protocol, + Host: p.Host, + Port: p.Port, + Username: p.Username, + Password: p.Password, + Status: p.Status, + ExpiresAt: expiresAt, + FallbackMode: p.FallbackMode, + BackupProxyName: backupProxyName, + ExpiryWarnDays: p.ExpiryWarnDays, }) } @@ -98,10 +118,15 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { } proxyByKey := make(map[string]service.Proxy, len(existingProxies)) + // proxyNameToID 用于 backup_proxy_name 反查:DB 已有 + 本批次新建均会写入 + proxyNameToID := make(map[string]int64, len(existingProxies)) for i := range existingProxies { p := existingProxies[i] key := buildProxyKey(p.Protocol, p.Host, p.Port, p.Username, p.Password) proxyByKey[key] = p + if p.Name != "" { + proxyNameToID[p.Name] = p.ID + } } latencyProbeIDs := make([]int64, 0, len(req.Data.Proxies)) @@ -127,7 +152,38 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { if existing, ok := proxyByKey[key]; ok { result.ProxyReused++ if normalizedStatus != "" && normalizedStatus != existing.Status { - if _, err := h.adminService.UpdateProxy(ctx, existing.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil { + // 已存在代理同步 status 时,同时保留/覆盖导入 item 的完整字段, + // 避免 UpdateProxy 零值覆盖有效期/fallback 配置。 + var existingExpiresAt *time.Time + if item.ExpiresAt != nil { + t := time.Unix(*item.ExpiresAt, 0).UTC() + existingExpiresAt = &t + } + existingFallbackMode := item.FallbackMode + if existingFallbackMode == "" { + existingFallbackMode = service.FallbackModeNone + } + var existingBackupProxyID *int64 + if item.BackupProxyName != "" { + if bid, ok := proxyNameToID[item.BackupProxyName]; ok { + existingBackupProxyID = &bid + } + } + updateInput := &service.UpdateProxyInput{ + Status: normalizedStatus, + ExpiresAt: existingExpiresAt, + FallbackMode: existingFallbackMode, + BackupProxyID: existingBackupProxyID, + ExpiryWarnDays: item.ExpiryWarnDays, + // 保留已存在代理的网络配置字段 + Name: existing.Name, + Protocol: existing.Protocol, + Host: existing.Host, + Port: existing.Port, + Username: existing.Username, + Password: existing.Password, + } + if _, err := h.adminService.UpdateProxy(ctx, existing.ID, updateInput); err != nil { result.Errors = append(result.Errors, DataImportError{ Kind: "proxy", Name: item.Name, @@ -140,13 +196,42 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { continue } + // 解析 expires_at(unix 秒 → *time.Time) + var expiresAt *time.Time + if item.ExpiresAt != nil { + t := time.Unix(*item.ExpiresAt, 0).UTC() + expiresAt = &t + } + + // 解析 backup_proxy_name → backup_proxy_id + fallbackMode := item.FallbackMode + var backupProxyID *int64 + if item.BackupProxyName != "" { + if bid, ok := proxyNameToID[item.BackupProxyName]; ok { + backupProxyID = &bid + } else { + // 查不到备用代理:降级 fallback_mode=none,记录 warning + fallbackMode = service.FallbackModeNone + result.Errors = append(result.Errors, DataImportError{ + Kind: "proxy", + Name: item.Name, + ProxyKey: key, + Message: fmt.Sprintf("backup_proxy_name %q not found, fallback_mode downgraded to none", item.BackupProxyName), + }) + } + } + created, err := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{ - Name: defaultProxyName(item.Name), - Protocol: item.Protocol, - Host: item.Host, - Port: item.Port, - Username: item.Username, - Password: item.Password, + Name: defaultProxyName(item.Name), + Protocol: item.Protocol, + Host: item.Host, + Port: item.Port, + Username: item.Username, + Password: item.Password, + ExpiresAt: expiresAt, + FallbackMode: fallbackMode, + BackupProxyID: backupProxyID, + ExpiryWarnDays: item.ExpiryWarnDays, }) if err != nil { result.ProxyFailed++ @@ -160,9 +245,26 @@ func (h *ProxyHandler) ImportData(c *gin.Context) { } result.ProxyCreated++ proxyByKey[key] = *created + // 把新建代理的 name 也加入反查表,供后续批内代理引用 + if created.Name != "" { + proxyNameToID[created.Name] = created.ID + } if normalizedStatus != "" && normalizedStatus != created.Status { - if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{Status: normalizedStatus}); err != nil { + // 新建后同步 status 时,传入完整字段,避免零值覆盖刚创建的有效期/fallback 配置。 + if _, err := h.adminService.UpdateProxy(ctx, created.ID, &service.UpdateProxyInput{ + Status: normalizedStatus, + ExpiresAt: expiresAt, + FallbackMode: fallbackMode, + BackupProxyID: backupProxyID, + ExpiryWarnDays: item.ExpiryWarnDays, + Name: created.Name, + Protocol: created.Protocol, + Host: created.Host, + Port: created.Port, + Username: created.Username, + Password: created.Password, + }); err != nil { result.Errors = append(result.Errors, DataImportError{ Kind: "proxy", Name: item.Name, diff --git a/backend/internal/handler/admin/proxy_handler.go b/backend/internal/handler/admin/proxy_handler.go index f97fcb0a71..93c729107a 100644 --- a/backend/internal/handler/admin/proxy_handler.go +++ b/backend/internal/handler/admin/proxy_handler.go @@ -4,6 +4,7 @@ import ( "context" "strconv" "strings" + "time" "github.com/Wei-Shaw/sub2api/internal/handler/dto" "github.com/Wei-Shaw/sub2api/internal/pkg/response" @@ -26,23 +27,31 @@ func NewProxyHandler(adminService service.AdminService) *ProxyHandler { // CreateProxyRequest represents create proxy request type CreateProxyRequest struct { - Name string `json:"name" binding:"required"` - Protocol string `json:"protocol" binding:"required,oneof=http https socks5 socks5h"` - Host string `json:"host" binding:"required"` - Port int `json:"port" binding:"required,min=1,max=65535"` - Username string `json:"username"` - Password string `json:"password"` + Name string `json:"name" binding:"required"` + Protocol string `json:"protocol" binding:"required,oneof=http https socks5 socks5h"` + Host string `json:"host" binding:"required"` + Port int `json:"port" binding:"required,min=1,max=65535"` + Username string `json:"username"` + Password string `json:"password"` + ExpiresAt *int64 `json:"expires_at"` + FallbackMode string `json:"fallback_mode" binding:"omitempty,oneof=none proxy direct"` + BackupProxyID *int64 `json:"backup_proxy_id"` + ExpiryWarnDays int `json:"expiry_warn_days" binding:"omitempty,min=0"` } // UpdateProxyRequest represents update proxy request type UpdateProxyRequest struct { - Name string `json:"name"` - Protocol string `json:"protocol" binding:"omitempty,oneof=http https socks5 socks5h"` - Host string `json:"host"` - Port int `json:"port" binding:"omitempty,min=1,max=65535"` - Username string `json:"username"` - Password string `json:"password"` - Status string `json:"status" binding:"omitempty,oneof=active inactive"` + Name string `json:"name"` + Protocol string `json:"protocol" binding:"omitempty,oneof=http https socks5 socks5h"` + Host string `json:"host"` + Port int `json:"port" binding:"omitempty,min=1,max=65535"` + Username string `json:"username"` + Password string `json:"password"` + Status string `json:"status" binding:"omitempty,oneof=active inactive"` + ExpiresAt *int64 `json:"expires_at"` + FallbackMode string `json:"fallback_mode" binding:"omitempty,oneof=none proxy direct"` + BackupProxyID *int64 `json:"backup_proxy_id"` + ExpiryWarnDays int `json:"expiry_warn_days" binding:"omitempty,min=0"` } // List handles listing all proxies with pagination @@ -134,13 +143,22 @@ func (h *ProxyHandler) Create(c *gin.Context) { } executeAdminIdempotentJSON(c, "admin.proxies.create", req, service.DefaultWriteIdempotencyTTL(), func(ctx context.Context) (any, error) { + var expiresAt *time.Time + if req.ExpiresAt != nil && *req.ExpiresAt > 0 { + t := time.Unix(*req.ExpiresAt, 0).UTC() + expiresAt = &t + } proxy, err := h.adminService.CreateProxy(ctx, &service.CreateProxyInput{ - Name: strings.TrimSpace(req.Name), - Protocol: strings.TrimSpace(req.Protocol), - Host: strings.TrimSpace(req.Host), - Port: req.Port, - Username: strings.TrimSpace(req.Username), - Password: strings.TrimSpace(req.Password), + Name: strings.TrimSpace(req.Name), + Protocol: strings.TrimSpace(req.Protocol), + Host: strings.TrimSpace(req.Host), + Port: req.Port, + Username: strings.TrimSpace(req.Username), + Password: strings.TrimSpace(req.Password), + ExpiresAt: expiresAt, + FallbackMode: strings.TrimSpace(req.FallbackMode), + BackupProxyID: req.BackupProxyID, + ExpiryWarnDays: req.ExpiryWarnDays, }) if err != nil { return nil, err @@ -164,14 +182,23 @@ func (h *ProxyHandler) Update(c *gin.Context) { return } + var expiresAt *time.Time + if req.ExpiresAt != nil && *req.ExpiresAt > 0 { + t := time.Unix(*req.ExpiresAt, 0).UTC() + expiresAt = &t + } proxy, err := h.adminService.UpdateProxy(c.Request.Context(), proxyID, &service.UpdateProxyInput{ - Name: strings.TrimSpace(req.Name), - Protocol: strings.TrimSpace(req.Protocol), - Host: strings.TrimSpace(req.Host), - Port: req.Port, - Username: strings.TrimSpace(req.Username), - Password: strings.TrimSpace(req.Password), - Status: strings.TrimSpace(req.Status), + Name: strings.TrimSpace(req.Name), + Protocol: strings.TrimSpace(req.Protocol), + Host: strings.TrimSpace(req.Host), + Port: req.Port, + Username: strings.TrimSpace(req.Username), + Password: strings.TrimSpace(req.Password), + Status: strings.TrimSpace(req.Status), + ExpiresAt: expiresAt, + FallbackMode: strings.TrimSpace(req.FallbackMode), + BackupProxyID: req.BackupProxyID, + ExpiryWarnDays: req.ExpiryWarnDays, }) if err != nil { response.ErrorFrom(c, err) diff --git a/backend/internal/handler/dto/mappers.go b/backend/internal/handler/dto/mappers.go index f5148f2974..415d18dcaa 100644 --- a/backend/internal/handler/dto/mappers.go +++ b/backend/internal/handler/dto/mappers.go @@ -211,6 +211,8 @@ func AccountFromServiceShallow(a *service.Account) *Account { CredentialsStatus: credsStatus, Extra: a.Extra, ProxyID: a.ProxyID, + ProxyFallbackOriginID: a.ProxyFallbackOriginID, + ProxyFallbackOriginName: a.ProxyFallbackOriginName, Concurrency: a.Concurrency, LoadFactor: a.LoadFactor, Priority: a.Priority, @@ -412,15 +414,19 @@ func ProxyFromService(p *service.Proxy) *Proxy { return nil } return &Proxy{ - ID: p.ID, - Name: p.Name, - Protocol: p.Protocol, - Host: p.Host, - Port: p.Port, - Username: p.Username, - Status: p.Status, - CreatedAt: p.CreatedAt, - UpdatedAt: p.UpdatedAt, + ID: p.ID, + Name: p.Name, + Protocol: p.Protocol, + Host: p.Host, + Port: p.Port, + Username: p.Username, + Status: p.Status, + CreatedAt: p.CreatedAt, + UpdatedAt: p.UpdatedAt, + ExpiresAt: p.ExpiresAt, + FallbackMode: p.FallbackMode, + BackupProxyID: p.BackupProxyID, + ExpiryWarnDays: p.ExpiryWarnDays, } } diff --git a/backend/internal/handler/dto/types.go b/backend/internal/handler/dto/types.go index 54fbd842cd..d8748baa34 100644 --- a/backend/internal/handler/dto/types.go +++ b/backend/internal/handler/dto/types.go @@ -160,21 +160,23 @@ type Account struct { Type string `json:"type"` // Credentials 经 RedactCredentials 处理后只含非敏感子键;敏感 token / api_key / 私钥 // 的存在性通过 CredentialsStatus(has_)暴露,原始值不返回前端。 - Credentials map[string]any `json:"credentials"` - CredentialsStatus map[string]bool `json:"credentials_status,omitempty"` - Extra map[string]any `json:"extra"` - ProxyID *int64 `json:"proxy_id"` - Concurrency int `json:"concurrency"` - LoadFactor *int `json:"load_factor,omitempty"` - Priority int `json:"priority"` - RateMultiplier float64 `json:"rate_multiplier"` - Status string `json:"status"` - ErrorMessage string `json:"error_message"` - LastUsedAt *time.Time `json:"last_used_at"` - ExpiresAt *int64 `json:"expires_at"` - AutoPauseOnExpired bool `json:"auto_pause_on_expired"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Credentials map[string]any `json:"credentials"` + CredentialsStatus map[string]bool `json:"credentials_status,omitempty"` + Extra map[string]any `json:"extra"` + ProxyID *int64 `json:"proxy_id"` + ProxyFallbackOriginID *int64 `json:"proxy_fallback_origin_id"` + ProxyFallbackOriginName *string `json:"proxy_fallback_origin_name,omitempty"` + Concurrency int `json:"concurrency"` + LoadFactor *int `json:"load_factor,omitempty"` + Priority int `json:"priority"` + RateMultiplier float64 `json:"rate_multiplier"` + Status string `json:"status"` + ErrorMessage string `json:"error_message"` + LastUsedAt *time.Time `json:"last_used_at"` + ExpiresAt *int64 `json:"expires_at"` + AutoPauseOnExpired bool `json:"auto_pause_on_expired"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` Schedulable bool `json:"schedulable"` @@ -279,6 +281,11 @@ type Proxy struct { Status string `json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + + ExpiresAt *time.Time `json:"expires_at"` + FallbackMode string `json:"fallback_mode"` + BackupProxyID *int64 `json:"backup_proxy_id"` + ExpiryWarnDays int `json:"expiry_warn_days"` } type ProxyWithAccountCount struct { diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index f149116b3e..53d5339522 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -1581,6 +1581,9 @@ func (r *accountRepository) accountsToService(ctx context.Context, accounts []*d if acc.ProxyID != nil { proxyIDs = append(proxyIDs, *acc.ProxyID) } + if acc.ProxyFallbackOriginID != nil { + proxyIDs = append(proxyIDs, *acc.ProxyFallbackOriginID) + } } proxyMap, err := r.loadProxies(ctx, proxyIDs) @@ -1603,6 +1606,13 @@ func (r *accountRepository) accountsToService(ctx context.Context, accounts []*d out.Proxy = proxy } } + out.ProxyFallbackOriginID = acc.ProxyFallbackOriginID + if acc.ProxyFallbackOriginID != nil { + if op, ok := proxyMap[*acc.ProxyFallbackOriginID]; ok && op != nil { + n := op.Name + out.ProxyFallbackOriginName = &n + } + } if groups, ok := groupsByAccount[acc.ID]; ok { out.Groups = groups } @@ -1754,6 +1764,7 @@ func accountEntityToService(m *dbent.Account) *service.Account { Credentials: copyJSONMap(m.Credentials), Extra: copyJSONMap(m.Extra), ProxyID: m.ProxyID, + ProxyFallbackOriginID: m.ProxyFallbackOriginID, Concurrency: m.Concurrency, Priority: m.Priority, RateMultiplier: &rateMultiplier, @@ -2046,3 +2057,23 @@ func (r *accountRepository) ResetQuotaUsed(ctx context.Context, id int64) error } return nil } + +// RevertProxyFallback 将账号的 proxy_id 切回 proxy_fallback_origin_id,并清空 origin 字段。 +// 仅当 proxy_fallback_origin_id IS NOT NULL 时执行更新; +// 若影响行数为 0,则返回 ErrAccountNotInFallback(账号存在但不在 fallback 状态)。 +func (r *accountRepository) RevertProxyFallback(ctx context.Context, accountID int64) error { + res, err := r.sql.ExecContext(ctx, ` + UPDATE accounts SET proxy_id=proxy_fallback_origin_id, proxy_fallback_origin_id=NULL, updated_at=NOW() + WHERE id=$1 AND proxy_fallback_origin_id IS NOT NULL AND deleted_at IS NULL`, accountID) + if err != nil { + return err + } + n, _ := res.RowsAffected() + if n == 0 { + return service.ErrAccountNotInFallback + } + if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventAccountChanged, &accountID, nil, nil); err != nil { + logger.LegacyPrintf("repository.account", "[SchedulerOutbox] revert fallback enqueue failed: account=%d err=%v", accountID, err) + } + return nil +} diff --git a/backend/internal/repository/proxy_expiry_integration_test.go b/backend/internal/repository/proxy_expiry_integration_test.go new file mode 100644 index 0000000000..d0cdc913b6 --- /dev/null +++ b/backend/internal/repository/proxy_expiry_integration_test.go @@ -0,0 +1,104 @@ +//go:build integration + +package repository + +import ( + "context" + "testing" + "time" + + dbent "github.com/Wei-Shaw/sub2api/ent" + "github.com/Wei-Shaw/sub2api/internal/service" + "github.com/stretchr/testify/suite" +) + +type ProxyExpirySuite struct { + suite.Suite + ctx context.Context + tx *dbent.Tx + repo *proxyRepository +} + +func (s *ProxyExpirySuite) SetupTest() { + s.ctx = context.Background() + s.tx = testEntTx(s.T()) + s.repo = newProxyRepositoryWithSQL(s.tx.Client(), s.tx) +} +func TestProxyExpirySuite(t *testing.T) { suite.Run(t, new(ProxyExpirySuite)) } + +func (s *ProxyExpirySuite) mkProxy(name, mode string, expiresAt *time.Time, backupID *int64) int64 { + p := &service.Proxy{Name: name, Protocol: "http", Host: "127.0.0.1", Port: 8080, + Status: service.StatusActive, FallbackMode: mode, ExpiryWarnDays: 7, + ExpiresAt: expiresAt, BackupProxyID: backupID} + s.Require().NoError(s.repo.Create(s.ctx, p)) + return p.ID +} + +func (s *ProxyExpirySuite) mkAccountWithProxy(proxyID int64) int64 { + var id int64 + err := scanSingleRow(s.ctx, s.tx, ` + INSERT INTO accounts (name, platform, type, credentials, extra, status, proxy_id, created_at, updated_at) + VALUES ($1,'claude','api','{}','{}','active',$2,NOW(),NOW()) RETURNING id`, + []any{"acc-" + time.Now().Format("150405.000000"), proxyID}, &id) + s.Require().NoError(err) + return id +} + +func (s *ProxyExpirySuite) accountProxyID(id int64) *int64 { + var pid *int64 + err := scanSingleRow(s.ctx, s.tx, `SELECT proxy_id FROM accounts WHERE id=$1`, []any{id}, &pid) + s.Require().NoError(err) + return pid +} + +func (s *ProxyExpirySuite) TestSweep_DirectMode() { + past := time.Now().Add(-time.Hour) + pid := s.mkProxy("p-direct", service.FallbackModeDirect, &past, nil) + aid := s.mkAccountWithProxy(pid) + + changed, err := s.repo.SweepExpiredProxies(s.ctx, time.Now()) + s.Require().NoError(err) + s.Require().GreaterOrEqual(changed, int64(1)) + + got, _ := s.repo.GetByID(s.ctx, pid) + s.Require().Equal(service.StatusExpired, got.Status) + s.Require().Nil(s.accountProxyID(aid)) + var origin *int64 + err = scanSingleRow(s.ctx, s.tx, `SELECT proxy_fallback_origin_id FROM accounts WHERE id=$1`, []any{aid}, &origin) + s.Require().NoError(err) + s.Require().NotNil(origin) + s.Require().Equal(pid, *origin) +} + +func (s *ProxyExpirySuite) TestSweep_ProxyMode_Healthy() { + future := time.Now().Add(24 * time.Hour) + past := time.Now().Add(-time.Hour) + backup := s.mkProxy("p-backup", service.FallbackModeNone, &future, nil) + pid := s.mkProxy("p-main", service.FallbackModeProxy, &past, &backup) + aid := s.mkAccountWithProxy(pid) + + _, err := s.repo.SweepExpiredProxies(s.ctx, time.Now()) + s.Require().NoError(err) + s.Require().Equal(backup, *s.accountProxyID(aid)) + var origin *int64 + err = scanSingleRow(s.ctx, s.tx, `SELECT proxy_fallback_origin_id FROM accounts WHERE id=$1`, []any{aid}, &origin) + s.Require().NoError(err) + s.Require().NotNil(origin) + s.Require().Equal(pid, *origin) +} + +func (s *ProxyExpirySuite) TestSweep_NoneMode_KeepsAccount() { + past := time.Now().Add(-time.Hour) + pid := s.mkProxy("p-none", service.FallbackModeNone, &past, nil) + aid := s.mkAccountWithProxy(pid) + + _, err := s.repo.SweepExpiredProxies(s.ctx, time.Now()) + s.Require().NoError(err) + got, _ := s.repo.GetByID(s.ctx, pid) + s.Require().Equal(service.StatusExpired, got.Status) + s.Require().Equal(pid, *s.accountProxyID(aid)) + var origin *int64 + err = scanSingleRow(s.ctx, s.tx, `SELECT proxy_fallback_origin_id FROM accounts WHERE id=$1`, []any{aid}, &origin) + s.Require().NoError(err) + s.Require().Nil(origin) +} diff --git a/backend/internal/repository/proxy_repo.go b/backend/internal/repository/proxy_repo.go index 60b2f069ec..b34c0cb559 100644 --- a/backend/internal/repository/proxy_repo.go +++ b/backend/internal/repository/proxy_repo.go @@ -5,30 +5,29 @@ import ( "database/sql" "sort" "strings" + "time" dbent "github.com/Wei-Shaw/sub2api/ent" "github.com/Wei-Shaw/sub2api/ent/proxy" - "github.com/Wei-Shaw/sub2api/internal/service" - + "github.com/Wei-Shaw/sub2api/internal/pkg/logger" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" + "github.com/Wei-Shaw/sub2api/internal/service" entsql "entgo.io/ent/dialect/sql" ) -type sqlQuerier interface { - QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) -} - +// sqlQuerier 已替换为 sqlExecutor(定义在 group_repo.go), +// proxyRepository 使用同一接口以支持 ExecContext。 type proxyRepository struct { client *dbent.Client - sql sqlQuerier + sql sqlExecutor } func NewProxyRepository(client *dbent.Client, sqlDB *sql.DB) service.ProxyRepository { return newProxyRepositoryWithSQL(client, sqlDB) } -func newProxyRepositoryWithSQL(client *dbent.Client, sqlq sqlQuerier) *proxyRepository { +func newProxyRepositoryWithSQL(client *dbent.Client, sqlq sqlExecutor) *proxyRepository { return &proxyRepository{client: client, sql: sqlq} } @@ -38,13 +37,21 @@ func (r *proxyRepository) Create(ctx context.Context, proxyIn *service.Proxy) er SetProtocol(proxyIn.Protocol). SetHost(proxyIn.Host). SetPort(proxyIn.Port). - SetStatus(proxyIn.Status) + SetStatus(proxyIn.Status). + SetFallbackMode(proxyIn.FallbackMode). + SetExpiryWarnDays(proxyIn.ExpiryWarnDays) if proxyIn.Username != "" { builder.SetUsername(proxyIn.Username) } if proxyIn.Password != "" { builder.SetPassword(proxyIn.Password) } + if proxyIn.ExpiresAt != nil { + builder.SetExpiresAt(*proxyIn.ExpiresAt) + } + if proxyIn.BackupProxyID != nil { + builder.SetBackupProxyID(*proxyIn.BackupProxyID) + } created, err := builder.Save(ctx) if err == nil { @@ -89,7 +96,9 @@ func (r *proxyRepository) Update(ctx context.Context, proxyIn *service.Proxy) er SetProtocol(proxyIn.Protocol). SetHost(proxyIn.Host). SetPort(proxyIn.Port). - SetStatus(proxyIn.Status) + SetStatus(proxyIn.Status). + SetFallbackMode(proxyIn.FallbackMode). + SetExpiryWarnDays(proxyIn.ExpiryWarnDays) if proxyIn.Username != "" { builder.SetUsername(proxyIn.Username) } else { @@ -100,6 +109,16 @@ func (r *proxyRepository) Update(ctx context.Context, proxyIn *service.Proxy) er } else { builder.ClearPassword() } + if proxyIn.ExpiresAt != nil { + builder.SetExpiresAt(*proxyIn.ExpiresAt) + } else { + builder.ClearExpiresAt() + } + if proxyIn.BackupProxyID != nil { + builder.SetBackupProxyID(*proxyIn.BackupProxyID) + } else { + builder.ClearBackupProxyID() + } updated, err := builder.Save(ctx) if err == nil { @@ -258,6 +277,12 @@ func proxyListOrder(params pagination.PaginationParams) []func(*entsql.Selector) field = proxy.FieldStatus case "created_at": field = proxy.FieldCreatedAt + case "expiry": + // expires_at 可空(NULL=永不过期)。不写显式 NULLS: + // dbent.Asc/Desc 不带 NULLS 子句,继承 PG 默认 + // (ASC→NULLS LAST、DESC→NULLS FIRST),即 NULL 视为最晚—— + // 升序垫底、降序置顶。 + field = proxy.FieldExpiresAt default: field = proxy.FieldID } @@ -417,14 +442,18 @@ func proxyEntityToService(m *dbent.Proxy) *service.Proxy { return nil } out := &service.Proxy{ - ID: m.ID, - Name: m.Name, - Protocol: m.Protocol, - Host: m.Host, - Port: m.Port, - Status: m.Status, - CreatedAt: m.CreatedAt, - UpdatedAt: m.UpdatedAt, + ID: m.ID, + Name: m.Name, + Protocol: m.Protocol, + Host: m.Host, + Port: m.Port, + Status: m.Status, + CreatedAt: m.CreatedAt, + UpdatedAt: m.UpdatedAt, + ExpiresAt: m.ExpiresAt, + FallbackMode: m.FallbackMode, + BackupProxyID: m.BackupProxyID, + ExpiryWarnDays: m.ExpiryWarnDays, } if m.Username != nil { out.Username = *m.Username @@ -443,3 +472,139 @@ func applyProxyEntityToService(dst *service.Proxy, src *dbent.Proxy) { dst.CreatedAt = src.CreatedAt dst.UpdatedAt = src.UpdatedAt } + +// ListAllForFallback 返回所有代理(含过期/非活跃),供改投逻辑使用。 +func (r *proxyRepository) ListAllForFallback(ctx context.Context) ([]service.Proxy, error) { + proxies, err := r.client.Proxy.Query().All(ctx) + if err != nil { + return nil, err + } + out := make([]service.Proxy, 0, len(proxies)) + for i := range proxies { + out = append(out, *proxyEntityToService(proxies[i])) + } + return out, nil +} + +// SweepExpiredProxies 扫描到期 active 代理,标记 expired 并按 fallback 策略改写绑定账号的 proxy_id, +// 最终触发 scheduler outbox 使 Redis 快照缓存失效。返回受影响的账号行数。 +// 原子性边界:每个过期代理的「标记 expired + 改投账号」在各自子事务内原子执行(见 sweepOneExpiredProxy); +// 全部代理处理完后若有账号被改投,再统一 enqueue 一次 full_rebuild 事件——该 enqueue 在子事务之外 +// (走 r.sql、失败仅记日志、由调度器周期性 full rebuild 兜底),故「改投 → 失效」整体并非原子。 +func (r *proxyRepository) SweepExpiredProxies(ctx context.Context, now time.Time) (int64, error) { + // 快照读(事务前):允许脏读不影响正确性,事务内已加锁写。 + all, err := r.ListAllForFallback(ctx) + if err != nil { + return 0, err + } + byID := make(map[int64]service.Proxy, len(all)) + for _, p := range all { + byID[p.ID] = p + } + + var totalChanged int64 + accountsTouched := false + + for _, p := range all { + if p.Status != service.StatusActive || !p.IsExpired(now) { + continue + } + + target, change := service.ResolveProxyFallbackTarget(p, byID, now) + if !change && p.FallbackMode == service.FallbackModeProxy { + // 配置了 proxy 回退但链路无解(成环或全部已过期),记录告警日志 + logger.LegacyPrintf("repository.proxy", "[ProxyExpiry] proxy %d expired but fallback chain unresolved (cycle/all-expired); accounts kept", p.ID) + } + + changed, sweepErr := r.sweepOneExpiredProxy(ctx, p.ID, target, change) + if sweepErr != nil { + return totalChanged, sweepErr + } + if changed > 0 { + totalChanged += changed + accountsTouched = true + } + } + + if accountsTouched { + if err := enqueueSchedulerOutbox(ctx, r.sql, service.SchedulerOutboxEventFullRebuild, nil, nil, nil); err != nil { + logger.LegacyPrintf("repository.proxy", "[SchedulerOutbox] enqueue proxy expiry rebuild failed: err=%v", err) + } + } + return totalChanged, nil +} + +// sweepOneExpiredProxy 在单事务内原子执行:标记代理 expired + 改投绑定账号。 +// 若 r.client 已绑定事务(测试注入场景),直接在 r.sql 上执行,由外层事务保证原子性。 +func (r *proxyRepository) sweepOneExpiredProxy(ctx context.Context, proxyID int64, target *int64, change bool) (int64, error) { + // 尝试开启子事务;若 r.client 已是事务 client,则返回 ErrTxStarted,退回使用 r.sql。 + tx, txErr := r.client.Tx(ctx) + if txErr != nil { + if txErr != dbent.ErrTxStarted { + return 0, txErr + } + // 已在外层事务中(集成测试场景),直接用 r.sql 执行 + return r.sweepOneExpiredProxyOnExec(ctx, r.sql, proxyID, target, change) + } + + // 使用新事务执行 + var n int64 + var err error + n, err = r.sweepOneExpiredProxyOnExec(ctx, tx, proxyID, target, change) + if err != nil { + _ = tx.Rollback() + return 0, err + } + if commitErr := tx.Commit(); commitErr != nil { + return 0, commitErr + } + return n, nil +} + +// sweepOneExpiredProxyOnExec 在给定的 sqlExecutor 上执行:标记 expired + 改投账号。 +func (r *proxyRepository) sweepOneExpiredProxyOnExec(ctx context.Context, exec sqlExecutor, proxyID int64, target *int64, change bool) (int64, error) { + if _, err := exec.ExecContext(ctx, + `UPDATE proxies SET status=$1, updated_at=NOW() WHERE id=$2 AND deleted_at IS NULL`, + service.StatusExpired, proxyID); err != nil { + return 0, err + } + if !change { + return 0, nil + } + var ( + res sql.Result + err error + ) + if target == nil { + res, err = exec.ExecContext(ctx, ` + UPDATE accounts SET proxy_id=NULL, proxy_fallback_origin_id=$1, updated_at=NOW() + WHERE proxy_id=$1 AND proxy_fallback_origin_id IS NULL AND deleted_at IS NULL`, proxyID) + } else { + res, err = exec.ExecContext(ctx, ` + UPDATE accounts SET proxy_id=$2, proxy_fallback_origin_id=$1, updated_at=NOW() + WHERE proxy_id=$1 AND proxy_fallback_origin_id IS NULL AND deleted_at IS NULL`, proxyID, *target) + } + if err != nil { + return 0, err + } + n, _ := res.RowsAffected() + return n, nil +} + +// CountExpired 返回已过期(status=expired)的代理数量。 +func (r *proxyRepository) CountExpired(ctx context.Context) (int64, error) { + var c int64 + err := scanSingleRow(ctx, r.sql, `SELECT COUNT(*) FROM proxies WHERE status=$1 AND deleted_at IS NULL`, []any{service.StatusExpired}, &c) + return c, err +} + +// CountExpiringSoon 返回即将到期(在 expiry_warn_days 天内)的活跃代理数量。 +func (r *proxyRepository) CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) { + var c int64 + err := scanSingleRow(ctx, r.sql, ` + SELECT COUNT(*) FROM proxies + WHERE deleted_at IS NULL AND status=$1 AND expires_at IS NOT NULL + AND expires_at > $2 AND expires_at <= $2 + (expiry_warn_days || ' days')::interval`, + []any{service.StatusActive, now}, &c) + return c, err +} diff --git a/backend/internal/repository/proxy_repo_sort_integration_test.go b/backend/internal/repository/proxy_repo_sort_integration_test.go index fe1c2873ba..af356c7d88 100644 --- a/backend/internal/repository/proxy_repo_sort_integration_test.go +++ b/backend/internal/repository/proxy_repo_sort_integration_test.go @@ -3,6 +3,8 @@ package repository import ( + "time" + "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/service" ) @@ -26,3 +28,41 @@ func (s *ProxyRepoSuite) TestListWithFiltersAndAccountCount_SortByAccountCountDe s.Require().Equal(int64(2), proxies[0].AccountCount) s.Require().Equal(p2.ID, proxies[1].ID) } + +func (s *ProxyRepoSuite) TestListWithFiltersAndAccountCount_SortByExpiry() { + now := time.Now() + past := now.Add(-24 * time.Hour) + soon := now.Add(72 * time.Hour) + later := now.Add(100 * 24 * time.Hour) + + // 创建顺序(=ID 升序)刻意打乱,使其不同于任何有效期顺序: + // 一旦排序退回按 id(没加 case "expiry"),此测试必然失败。 + pLater := s.mustCreateProxy(&service.Proxy{Name: "p-later", Protocol: "http", Host: "127.0.0.1", Port: 8080, Status: service.StatusActive, ExpiresAt: &later}) + pNever := s.mustCreateProxy(&service.Proxy{Name: "p-never", Protocol: "http", Host: "127.0.0.1", Port: 8081, Status: service.StatusActive, ExpiresAt: nil}) + pExpired := s.mustCreateProxy(&service.Proxy{Name: "p-expired", Protocol: "http", Host: "127.0.0.1", Port: 8082, Status: service.StatusActive, ExpiresAt: &past}) + pSoon := s.mustCreateProxy(&service.Proxy{Name: "p-soon", Protocol: "http", Host: "127.0.0.1", Port: 8083, Status: service.StatusActive, ExpiresAt: &soon}) + + // 升序:最快到期在前,NULL(永不过期)垫底 + asc, _, err := s.repo.ListWithFiltersAndAccountCount(s.ctx, pagination.PaginationParams{ + Page: 1, PageSize: 10, SortBy: "expiry", SortOrder: "asc", + }, "", "", "") + s.Require().NoError(err) + s.Require().Len(asc, 4) + s.Require().Equal( + []int64{pExpired.ID, pSoon.ID, pLater.ID, pNever.ID}, + []int64{asc[0].ID, asc[1].ID, asc[2].ID, asc[3].ID}, + "asc: 过期→快到期→远期→永不过期(垫底)", + ) + + // 降序:NULL(永不过期)置顶 + desc, _, err := s.repo.ListWithFiltersAndAccountCount(s.ctx, pagination.PaginationParams{ + Page: 1, PageSize: 10, SortBy: "expiry", SortOrder: "desc", + }, "", "", "") + s.Require().NoError(err) + s.Require().Len(desc, 4) + s.Require().Equal( + []int64{pNever.ID, pLater.ID, pSoon.ID, pExpired.ID}, + []int64{desc[0].ID, desc[1].ID, desc[2].ID, desc[3].ID}, + "desc: 永不过期(置顶)→远期→快到期→过期", + ) +} diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 54719cda2e..7ed9959792 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -1796,6 +1796,10 @@ func (s *stubAccountRepo) ListCRSAccountIDs(ctx context.Context) (map[string]int return nil, errors.New("not implemented") } +func (s *stubAccountRepo) RevertProxyFallback(ctx context.Context, accountID int64) error { + return nil +} + type stubProxyRepo struct{} func (stubProxyRepo) Create(ctx context.Context, proxy *service.Proxy) error { @@ -1850,6 +1854,22 @@ func (stubProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID return nil, errors.New("not implemented") } +func (stubProxyRepo) SweepExpiredProxies(ctx context.Context, now time.Time) (int64, error) { + return 0, nil +} + +func (stubProxyRepo) ListAllForFallback(ctx context.Context) ([]service.Proxy, error) { + return nil, nil +} + +func (stubProxyRepo) CountExpired(ctx context.Context) (int64, error) { + return 0, nil +} + +func (stubProxyRepo) CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) { + return 0, nil +} + type stubRedeemCodeRepo struct { byUser map[int64][]service.RedeemCode } diff --git a/backend/internal/server/routes/admin.go b/backend/internal/server/routes/admin.go index 9a3253b55d..0045231239 100644 --- a/backend/internal/server/routes/admin.go +++ b/backend/internal/server/routes/admin.go @@ -294,6 +294,7 @@ func registerAccountRoutes(admin *gin.RouterGroup, h *handler.Handlers) { accounts.POST("/:id/refresh-tier", h.Admin.Account.RefreshTier) accounts.GET("/:id/stats", h.Admin.Account.GetStats) accounts.POST("/:id/clear-error", h.Admin.Account.ClearError) + accounts.POST("/:id/revert-proxy-fallback", h.Admin.Account.RevertProxyFallback) accounts.GET("/:id/usage", h.Admin.Account.GetUsage) accounts.GET("/:id/today-stats", h.Admin.Account.GetTodayStats) accounts.POST("/today-stats/batch", h.Admin.Account.GetBatchTodayStats) diff --git a/backend/internal/service/account.go b/backend/internal/service/account.go index de7d97f884..42c7674462 100644 --- a/backend/internal/service/account.go +++ b/backend/internal/service/account.go @@ -17,16 +17,18 @@ import ( ) type Account struct { - ID int64 - Name string - Notes *string - Platform string - Type string - Credentials map[string]any - Extra map[string]any - ProxyID *int64 - Concurrency int - Priority int + ID int64 + Name string + Notes *string + Platform string + Type string + Credentials map[string]any + Extra map[string]any + ProxyID *int64 + ProxyFallbackOriginID *int64 + ProxyFallbackOriginName *string // 仅展示用 + Concurrency int + Priority int // RateMultiplier 账号计费倍率(>=0,允许 0 表示该账号计费为 0)。 // 使用指针用于兼容旧版本调度缓存(Redis)中缺字段的情况:nil 表示按 1.0 处理。 RateMultiplier *float64 diff --git a/backend/internal/service/account_service.go b/backend/internal/service/account_service.go index 748840b75d..5f0ac3fca2 100644 --- a/backend/internal/service/account_service.go +++ b/backend/internal/service/account_service.go @@ -10,8 +10,9 @@ import ( ) var ( - ErrAccountNotFound = infraerrors.NotFound("ACCOUNT_NOT_FOUND", "account not found") - ErrAccountNilInput = infraerrors.BadRequest("ACCOUNT_NIL_INPUT", "account input cannot be nil") + ErrAccountNotFound = infraerrors.NotFound("ACCOUNT_NOT_FOUND", "account not found") + ErrAccountNilInput = infraerrors.BadRequest("ACCOUNT_NIL_INPUT", "account input cannot be nil") + ErrAccountNotInFallback = infraerrors.BadRequest("ACCOUNT_NOT_IN_FALLBACK", "account is not in proxy fallback state") ) const AccountListGroupUngrouped int64 = -1 @@ -74,6 +75,9 @@ type AccountRepository interface { IncrementQuotaUsed(ctx context.Context, id int64, amount float64) error // ResetQuotaUsed 重置 API Key 账号所有维度的配额用量为 0 ResetQuotaUsed(ctx context.Context, id int64) error + // RevertProxyFallback 将账号的 proxy_id 切回 proxy_fallback_origin_id,并清空 origin 字段。 + // 仅当 proxy_fallback_origin_id IS NOT NULL 时更新,否则视为账号不存在(返回 ErrAccountNotFound)。 + RevertProxyFallback(ctx context.Context, accountID int64) error } // AccountBulkUpdate describes the fields that can be updated in a bulk operation. diff --git a/backend/internal/service/account_service_delete_test.go b/backend/internal/service/account_service_delete_test.go index d72554ce1f..1d2961ab3f 100644 --- a/backend/internal/service/account_service_delete_test.go +++ b/backend/internal/service/account_service_delete_test.go @@ -207,6 +207,10 @@ func (s *accountRepoStub) ResetQuotaUsed(ctx context.Context, id int64) error { return nil } +func (s *accountRepoStub) RevertProxyFallback(ctx context.Context, accountID int64) error { + panic("unexpected RevertProxyFallback call") +} + // TestAccountService_Delete_NotFound 测试删除不存在的账号时返回正确的错误。 // 预期行为: // - ExistsByID 返回 false(账号不存在) diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index cad6e8485e..c55f8026c8 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -96,6 +96,9 @@ type AdminService interface { SetAccountSchedulable(ctx context.Context, id int64, schedulable bool) (*Account, error) BulkUpdateAccounts(ctx context.Context, input *BulkUpdateAccountsInput) (*BulkUpdateAccountsResult, error) CheckMixedChannelRisk(ctx context.Context, currentAccountID int64, currentAccountPlatform string, groupIDs []int64) error + // RevertAccountProxyFallback 将账号的 proxy_id 切回 proxy_fallback_origin_id,并清空 origin 字段。 + // 若账号不存在返回 ErrAccountNotFound;若账号存在但不在 fallback 状态,返回 ErrAccountNotInFallback。 + RevertAccountProxyFallback(ctx context.Context, id int64) error // Proxy management ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error) @@ -384,22 +387,30 @@ type BulkUpdateAccountsResult struct { } type CreateProxyInput struct { - Name string - Protocol string - Host string - Port int - Username string - Password string + Name string + Protocol string + Host string + Port int + Username string + Password string + ExpiresAt *time.Time + FallbackMode string + BackupProxyID *int64 + ExpiryWarnDays int } type UpdateProxyInput struct { - Name string - Protocol string - Host string - Port int - Username string - Password string - Status string + Name string + Protocol string + Host string + Port int + Username string + Password string + Status string + ExpiresAt *time.Time + FallbackMode string + BackupProxyID *int64 + ExpiryWarnDays int } type GenerateRedeemCodesInput struct { @@ -2998,6 +3009,10 @@ func (s *adminServiceImpl) SetAccountSchedulable(ctx context.Context, id int64, return updated, nil } +func (s *adminServiceImpl) RevertAccountProxyFallback(ctx context.Context, id int64) error { + return s.accountRepo.RevertProxyFallback(ctx, id) +} + // Proxy management implementations func (s *adminServiceImpl) ListProxies(ctx context.Context, page, pageSize int, protocol, status, search string, sortBy, sortOrder string) ([]Proxy, int64, error) { params := pagination.PaginationParams{Page: page, PageSize: pageSize, SortBy: sortBy, SortOrder: sortOrder} @@ -3040,14 +3055,31 @@ func (s *adminServiceImpl) GetProxiesByIDs(ctx context.Context, ids []int64) ([] } func (s *adminServiceImpl) CreateProxy(ctx context.Context, input *CreateProxyInput) (*Proxy, error) { + // 规范化 fallback_mode + mode := input.FallbackMode + if mode == "" { + mode = FallbackModeNone + } + // 校验:mode=proxy 必须有 backup + if mode == FallbackModeProxy && input.BackupProxyID == nil { + return nil, infraerrors.BadRequest("PROXY_BACKUP_REQUIRED", "backup proxy required when fallback_mode=proxy") + } + if input.ExpiryWarnDays < 0 { + return nil, infraerrors.BadRequest("PROXY_WARN_DAYS_INVALID", "expiry_warn_days must be >= 0") + } + proxy := &Proxy{ - Name: input.Name, - Protocol: input.Protocol, - Host: input.Host, - Port: input.Port, - Username: input.Username, - Password: input.Password, - Status: StatusActive, + Name: input.Name, + Protocol: input.Protocol, + Host: input.Host, + Port: input.Port, + Username: input.Username, + Password: input.Password, + Status: StatusActive, + ExpiresAt: input.ExpiresAt, + FallbackMode: mode, + BackupProxyID: input.BackupProxyID, + ExpiryWarnDays: input.ExpiryWarnDays, } if err := s.proxyRepo.Create(ctx, proxy); err != nil { return nil, err @@ -3058,6 +3090,23 @@ func (s *adminServiceImpl) CreateProxy(ctx context.Context, input *CreateProxyIn } func (s *adminServiceImpl) UpdateProxy(ctx context.Context, id int64, input *UpdateProxyInput) (*Proxy, error) { + // 校验:backup_proxy_id 不能是自身 + if input.BackupProxyID != nil && *input.BackupProxyID == id { + return nil, infraerrors.BadRequest("PROXY_BACKUP_SELF", "backup proxy cannot be itself") + } + // 规范化 fallback_mode + mode := input.FallbackMode + if mode == "" { + mode = FallbackModeNone + } + // 校验:mode=proxy 必须有 backup + if mode == FallbackModeProxy && input.BackupProxyID == nil { + return nil, infraerrors.BadRequest("PROXY_BACKUP_REQUIRED", "backup proxy required when fallback_mode=proxy") + } + if input.ExpiryWarnDays < 0 { + return nil, infraerrors.BadRequest("PROXY_WARN_DAYS_INVALID", "expiry_warn_days must be >= 0") + } + proxy, err := s.proxyRepo.GetByID(ctx, id) if err != nil { return nil, err @@ -3084,6 +3133,11 @@ func (s *adminServiceImpl) UpdateProxy(ctx context.Context, id int64, input *Upd if input.Status != "" { proxy.Status = input.Status } + // 透传有效期与回退字段 + proxy.ExpiresAt = input.ExpiresAt + proxy.FallbackMode = mode + proxy.BackupProxyID = input.BackupProxyID + proxy.ExpiryWarnDays = input.ExpiryWarnDays if err := s.proxyRepo.Update(ctx, proxy); err != nil { return nil, err diff --git a/backend/internal/service/admin_service_delete_test.go b/backend/internal/service/admin_service_delete_test.go index 717710f962..59974f9ad5 100644 --- a/backend/internal/service/admin_service_delete_test.go +++ b/backend/internal/service/admin_service_delete_test.go @@ -325,6 +325,18 @@ func (s *proxyRepoStub) CountAccountsByProxyID(ctx context.Context, proxyID int6 func (s *proxyRepoStub) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]ProxyAccountSummary, error) { panic("unexpected ListAccountSummariesByProxyID call") } +func (s *proxyRepoStub) SweepExpiredProxies(_ context.Context, _ time.Time) (int64, error) { + return 0, nil +} +func (s *proxyRepoStub) ListAllForFallback(_ context.Context) ([]Proxy, error) { + return nil, nil +} +func (s *proxyRepoStub) CountExpired(_ context.Context) (int64, error) { + return 0, nil +} +func (s *proxyRepoStub) CountExpiringSoon(_ context.Context, _ time.Time) (int64, error) { + return 0, nil +} type redeemRepoStub struct { deleteErrByID map[int64]error diff --git a/backend/internal/service/gateway_multiplatform_test.go b/backend/internal/service/gateway_multiplatform_test.go index 7a6acaaca8..0d0995329d 100644 --- a/backend/internal/service/gateway_multiplatform_test.go +++ b/backend/internal/service/gateway_multiplatform_test.go @@ -195,6 +195,10 @@ func (m *mockAccountRepoForPlatform) ResetQuotaUsed(ctx context.Context, id int6 return nil } +func (m *mockAccountRepoForPlatform) RevertProxyFallback(ctx context.Context, accountID int64) error { + return nil +} + // Verify interface implementation var _ AccountRepository = (*mockAccountRepoForPlatform)(nil) diff --git a/backend/internal/service/gemini_multiplatform_test.go b/backend/internal/service/gemini_multiplatform_test.go index 8f879b0238..f7a17a7441 100644 --- a/backend/internal/service/gemini_multiplatform_test.go +++ b/backend/internal/service/gemini_multiplatform_test.go @@ -184,6 +184,10 @@ func (m *mockAccountRepoForGemini) ResetQuotaUsed(ctx context.Context, id int64) return nil } +func (m *mockAccountRepoForGemini) RevertProxyFallback(ctx context.Context, accountID int64) error { + return nil +} + // Verify interface implementation var _ AccountRepository = (*mockAccountRepoForGemini)(nil) diff --git a/backend/internal/service/gemini_oauth_service_test.go b/backend/internal/service/gemini_oauth_service_test.go index 397b581d12..887ec79bf6 100644 --- a/backend/internal/service/gemini_oauth_service_test.go +++ b/backend/internal/service/gemini_oauth_service_test.go @@ -805,6 +805,18 @@ func (m *mockGeminiProxyRepo) CountAccountsByProxyID(ctx context.Context, proxyI func (m *mockGeminiProxyRepo) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]ProxyAccountSummary, error) { panic("not impl") } +func (m *mockGeminiProxyRepo) SweepExpiredProxies(ctx context.Context, now time.Time) (int64, error) { + panic("not impl") +} +func (m *mockGeminiProxyRepo) ListAllForFallback(ctx context.Context) ([]Proxy, error) { + panic("not impl") +} +func (m *mockGeminiProxyRepo) CountExpired(ctx context.Context) (int64, error) { + panic("not impl") +} +func (m *mockGeminiProxyRepo) CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) { + panic("not impl") +} // mockDriveClient implements geminicli.DriveClient for tests. type mockDriveClient struct { diff --git a/backend/internal/service/oauth_service_test.go b/backend/internal/service/oauth_service_test.go index 78f39dc57b..e86c46bd36 100644 --- a/backend/internal/service/oauth_service_test.go +++ b/backend/internal/service/oauth_service_test.go @@ -6,6 +6,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/Wei-Shaw/sub2api/internal/pkg/oauth" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" @@ -96,6 +97,18 @@ func (m *mockProxyRepoForOAuth) CountAccountsByProxyID(ctx context.Context, prox func (m *mockProxyRepoForOAuth) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]ProxyAccountSummary, error) { panic("ListAccountSummariesByProxyID not implemented") } +func (m *mockProxyRepoForOAuth) SweepExpiredProxies(ctx context.Context, now time.Time) (int64, error) { + panic("SweepExpiredProxies not implemented") +} +func (m *mockProxyRepoForOAuth) ListAllForFallback(ctx context.Context) ([]Proxy, error) { + panic("ListAllForFallback not implemented") +} +func (m *mockProxyRepoForOAuth) CountExpired(ctx context.Context) (int64, error) { + panic("CountExpired not implemented") +} +func (m *mockProxyRepoForOAuth) CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) { + panic("CountExpiringSoon not implemented") +} // ===================== // 测试用例 diff --git a/backend/internal/service/ops_alert_evaluator_service.go b/backend/internal/service/ops_alert_evaluator_service.go index 4d3652766a..5ee54601ca 100644 --- a/backend/internal/service/ops_alert_evaluator_service.go +++ b/backend/internal/service/ops_alert_evaluator_service.go @@ -35,6 +35,7 @@ type OpsAlertEvaluatorService struct { opsService *OpsService opsRepo OpsRepository emailService *EmailService + proxyRepo ProxyRepository redisClient *redis.Client cfg *config.Config @@ -67,11 +68,13 @@ func NewOpsAlertEvaluatorService( emailService *EmailService, redisClient *redis.Client, cfg *config.Config, + proxyRepo ProxyRepository, ) *OpsAlertEvaluatorService { return &OpsAlertEvaluatorService{ opsService: opsService, opsRepo: opsRepo, emailService: emailService, + proxyRepo: proxyRepo, redisClient: redisClient, cfg: cfg, instanceID: uuid.NewString(), @@ -560,6 +563,24 @@ func (s *OpsAlertEvaluatorService) computeRuleMetric( return float64(countAccountsByCondition(availability.Accounts, func(acc *AccountAvailability) bool { return acc.IsOverloaded })), true + case "proxy_expired_count": + if s == nil || s.proxyRepo == nil { + return 0, false + } + n, err := s.proxyRepo.CountExpired(ctx) + if err != nil { + return 0, false + } + return float64(n), true + case "proxy_expiring_soon_count": + if s == nil || s.proxyRepo == nil { + return 0, false + } + n, err := s.proxyRepo.CountExpiringSoon(ctx, time.Now()) + if err != nil { + return 0, false + } + return float64(n), true } overview, err := s.opsRepo.GetDashboardOverview(ctx, &OpsDashboardFilter{ diff --git a/backend/internal/service/proxy.go b/backend/internal/service/proxy.go index a2896d6c14..afd673a9f9 100644 --- a/backend/internal/service/proxy.go +++ b/backend/internal/service/proxy.go @@ -7,23 +7,38 @@ import ( "time" ) +const ( + FallbackModeNone = "none" + FallbackModeProxy = "proxy" + FallbackModeDirect = "direct" +) + type Proxy struct { - ID int64 - Name string - Protocol string - Host string - Port int - Username string - Password string - Status string - CreatedAt time.Time - UpdatedAt time.Time + ID int64 + Name string + Protocol string + Host string + Port int + Username string + Password string + Status string + CreatedAt time.Time + UpdatedAt time.Time + ExpiresAt *time.Time + FallbackMode string + BackupProxyID *int64 + ExpiryWarnDays int } func (p *Proxy) IsActive() bool { return p.Status == StatusActive } +// IsExpired 报告代理是否已过期(基于 expires_at,与 status 无关)。 +func (p *Proxy) IsExpired(now time.Time) bool { + return p.ExpiresAt != nil && !p.ExpiresAt.After(now) +} + func (p *Proxy) URL() string { u := &url.URL{ Scheme: p.Protocol, diff --git a/backend/internal/service/proxy_expiry_service.go b/backend/internal/service/proxy_expiry_service.go new file mode 100644 index 0000000000..6be1217854 --- /dev/null +++ b/backend/internal/service/proxy_expiry_service.go @@ -0,0 +1,63 @@ +package service + +import ( + "context" + "log" + "sync" + "time" +) + +// ProxyExpiryService 周期扫描到期代理并把绑定账号改投备用/直连。 +type ProxyExpiryService struct { + proxyRepo ProxyRepository + interval time.Duration + stopCh chan struct{} + stopOnce sync.Once + wg sync.WaitGroup +} + +func NewProxyExpiryService(proxyRepo ProxyRepository, interval time.Duration) *ProxyExpiryService { + return &ProxyExpiryService{proxyRepo: proxyRepo, interval: interval, stopCh: make(chan struct{})} +} + +func (s *ProxyExpiryService) Start() { + if s == nil || s.proxyRepo == nil || s.interval <= 0 { + return + } + s.wg.Add(1) + go func() { + defer s.wg.Done() + ticker := time.NewTicker(s.interval) + defer ticker.Stop() + s.runOnce() + for { + select { + case <-ticker.C: + s.runOnce() + case <-s.stopCh: + return + } + } + }() +} + +func (s *ProxyExpiryService) Stop() { + if s == nil { + return + } + s.stopOnce.Do(func() { close(s.stopCh) }) + s.wg.Wait() +} + +func (s *ProxyExpiryService) runOnce() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + changed, err := s.proxyRepo.SweepExpiredProxies(ctx, time.Now()) + if err != nil { + log.Printf("[ProxyExpiry] sweep expired proxies failed: %v", err) + return + } + if changed > 0 { + log.Printf("[ProxyExpiry] re-routed %d accounts off expired proxies", changed) + } +} diff --git a/backend/internal/service/proxy_fallback.go b/backend/internal/service/proxy_fallback.go new file mode 100644 index 0000000000..aae32d0f32 --- /dev/null +++ b/backend/internal/service/proxy_fallback.go @@ -0,0 +1,47 @@ +package service + +import "time" + +// ResolveProxyFallbackTarget 计算一个过期代理 start 应把账号改投到哪里。 +// 返回 (targetID, change): +// - change=false:不改动账号(mode=none,或链路成环/无解的兜底) +// - change=true, targetID=nil:改投为直连 +// - change=true, targetID!=nil:改投到该备用代理 id +// +// byID 是「全部代理」的快照(id -> Proxy),now 为判定基准时间。 +func ResolveProxyFallbackTarget(start Proxy, byID map[int64]Proxy, now time.Time) (*int64, bool) { + switch start.FallbackMode { + case FallbackModeDirect: + return nil, true + case FallbackModeProxy: + visited := map[int64]struct{}{start.ID: {}} + curID := start.BackupProxyID + for { + if curID == nil { + return nil, false + } + if _, seen := visited[*curID]; seen { + return nil, false + } + p, ok := byID[*curID] + if !ok { + return nil, false + } + if !(&p).IsExpired(now) && p.Status != StatusExpired { + id := p.ID + return &id, true + } + visited[*curID] = struct{}{} + switch p.FallbackMode { + case FallbackModeDirect: + return nil, true + case FallbackModeProxy: + curID = p.BackupProxyID + default: + return nil, false + } + } + default: + return nil, false + } +} diff --git a/backend/internal/service/proxy_fallback_test.go b/backend/internal/service/proxy_fallback_test.go new file mode 100644 index 0000000000..c0223a5028 --- /dev/null +++ b/backend/internal/service/proxy_fallback_test.go @@ -0,0 +1,73 @@ +//go:build unit + +package service + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func mkProxy(id int64, mode string, backup *int64, expiresInDays *int, now time.Time) Proxy { + p := Proxy{ID: id, FallbackMode: mode, BackupProxyID: backup} + if expiresInDays != nil { + t := now.AddDate(0, 0, *expiresInDays) + p.ExpiresAt = &t + } + return p +} +func i64(v int64) *int64 { return &v } +func di(v int) *int { return &v } + +func TestResolveFallbackTarget(t *testing.T) { + now := time.Now() + t.Run("none keeps original", func(t *testing.T) { + a := mkProxy(1, FallbackModeNone, nil, di(-1), now) + by := map[int64]Proxy{1: a} + target, change := ResolveProxyFallbackTarget(a, by, now) + require.False(t, change) + require.Nil(t, target) + }) + t.Run("direct -> nil target, change", func(t *testing.T) { + a := mkProxy(1, FallbackModeDirect, nil, di(-1), now) + by := map[int64]Proxy{1: a} + target, change := ResolveProxyFallbackTarget(a, by, now) + require.True(t, change) + require.Nil(t, target) + }) + t.Run("proxy -> healthy backup", func(t *testing.T) { + b := mkProxy(2, FallbackModeNone, nil, di(30), now) + a := mkProxy(1, FallbackModeProxy, i64(2), di(-1), now) + by := map[int64]Proxy{1: a, 2: b} + target, change := ResolveProxyFallbackTarget(a, by, now) + require.True(t, change) + require.NotNil(t, target) + require.Equal(t, int64(2), *target) + }) + t.Run("chain A->B(expired)->C(healthy)", func(t *testing.T) { + c := mkProxy(3, FallbackModeNone, nil, di(30), now) + b := mkProxy(2, FallbackModeProxy, i64(3), di(-1), now) + a := mkProxy(1, FallbackModeProxy, i64(2), di(-1), now) + by := map[int64]Proxy{1: a, 2: b, 3: c} + target, change := ResolveProxyFallbackTarget(a, by, now) + require.True(t, change) + require.Equal(t, int64(3), *target) + }) + t.Run("cycle A->B->A keeps original", func(t *testing.T) { + b := mkProxy(2, FallbackModeProxy, i64(1), di(-1), now) + a := mkProxy(1, FallbackModeProxy, i64(2), di(-1), now) + by := map[int64]Proxy{1: a, 2: b} + target, change := ResolveProxyFallbackTarget(a, by, now) + require.False(t, change) + require.Nil(t, target) + }) + t.Run("chain tail direct fallback", func(t *testing.T) { + b := mkProxy(2, FallbackModeDirect, nil, di(-1), now) + a := mkProxy(1, FallbackModeProxy, i64(2), di(-1), now) + by := map[int64]Proxy{1: a, 2: b} + target, change := ResolveProxyFallbackTarget(a, by, now) + require.True(t, change) + require.Nil(t, target) + }) +} diff --git a/backend/internal/service/proxy_service.go b/backend/internal/service/proxy_service.go index 8004518762..957c0238db 100644 --- a/backend/internal/service/proxy_service.go +++ b/backend/internal/service/proxy_service.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "time" infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination" @@ -29,6 +30,11 @@ type ProxyRepository interface { ExistsByHostPortAuth(ctx context.Context, host string, port int, username, password string) (bool, error) CountAccountsByProxyID(ctx context.Context, proxyID int64) (int64, error) ListAccountSummariesByProxyID(ctx context.Context, proxyID int64) ([]ProxyAccountSummary, error) + + SweepExpiredProxies(ctx context.Context, now time.Time) (changed int64, err error) + ListAllForFallback(ctx context.Context) ([]Proxy, error) + CountExpired(ctx context.Context) (int64, error) + CountExpiringSoon(ctx context.Context, now time.Time) (int64, error) } // CreateProxyRequest 创建代理请求 diff --git a/backend/internal/service/ratelimit_session_window_test.go b/backend/internal/service/ratelimit_session_window_test.go index be6cb30975..85be77fbc3 100644 --- a/backend/internal/service/ratelimit_session_window_test.go +++ b/backend/internal/service/ratelimit_session_window_test.go @@ -153,6 +153,9 @@ func (m *sessionWindowMockRepo) IncrementQuotaUsed(context.Context, int64, float panic("unexpected") } func (m *sessionWindowMockRepo) ResetQuotaUsed(context.Context, int64) error { panic("unexpected") } +func (m *sessionWindowMockRepo) RevertProxyFallback(context.Context, int64) error { + panic("unexpected") +} // newRateLimitServiceForTest creates a RateLimitService with the given mock repo. func newRateLimitServiceForTest(repo AccountRepository) *RateLimitService { diff --git a/backend/internal/service/wire.go b/backend/internal/service/wire.go index 7eada0b3c2..f8721ba031 100644 --- a/backend/internal/service/wire.go +++ b/backend/internal/service/wire.go @@ -164,6 +164,13 @@ func ProvideAccountExpiryService(accountRepo AccountRepository) *AccountExpirySe return svc } +// ProvideProxyExpiryService creates and starts ProxyExpiryService. +func ProvideProxyExpiryService(proxyRepo ProxyRepository) *ProxyExpiryService { + svc := NewProxyExpiryService(proxyRepo, time.Minute) + svc.Start() + return svc +} + // ProvideSubscriptionExpiryService creates and starts SubscriptionExpiryService. func ProvideSubscriptionExpiryService(userSubRepo UserSubscriptionRepository, settingRepo SettingRepository, notificationEmailService *NotificationEmailService, lockCache LeaderLockCache, db *sql.DB) *SubscriptionExpiryService { svc := NewSubscriptionExpiryService(userSubRepo, time.Minute) @@ -281,8 +288,9 @@ func ProvideOpsAlertEvaluatorService( emailService *EmailService, redisClient *redis.Client, cfg *config.Config, + proxyRepo ProxyRepository, ) *OpsAlertEvaluatorService { - svc := NewOpsAlertEvaluatorService(opsService, opsRepo, emailService, redisClient, cfg) + svc := NewOpsAlertEvaluatorService(opsService, opsRepo, emailService, redisClient, cfg, proxyRepo) svc.Start() return svc } @@ -555,6 +563,7 @@ var ProviderSet = wire.NewSet( ProvideUpdateService, ProvideTokenRefreshService, ProvideAccountExpiryService, + ProvideProxyExpiryService, ProvideSubscriptionExpiryService, ProvideTimingWheelService, ProvideDashboardAggregationService, diff --git a/backend/migrations/149_proxy_expiry_fallback.sql b/backend/migrations/149_proxy_expiry_fallback.sql new file mode 100644 index 0000000000..1deb69436f --- /dev/null +++ b/backend/migrations/149_proxy_expiry_fallback.sql @@ -0,0 +1,11 @@ +-- proxies: 有效期 + 失败回退 +ALTER TABLE proxies ADD COLUMN IF NOT EXISTS expires_at timestamptz; +ALTER TABLE proxies ADD COLUMN IF NOT EXISTS fallback_mode varchar(20) NOT NULL DEFAULT 'none'; +ALTER TABLE proxies ADD COLUMN IF NOT EXISTS backup_proxy_id BIGINT REFERENCES proxies(id) ON DELETE SET NULL; +ALTER TABLE proxies ADD COLUMN IF NOT EXISTS expiry_warn_days INT NOT NULL DEFAULT 7; +CREATE INDEX IF NOT EXISTS proxies_expires_at_idx ON proxies (expires_at); +CREATE INDEX IF NOT EXISTS proxies_backup_proxy_id_idx ON proxies (backup_proxy_id); + +-- accounts: fallback 来源(手动回切用) +ALTER TABLE accounts ADD COLUMN IF NOT EXISTS proxy_fallback_origin_id BIGINT; +CREATE INDEX IF NOT EXISTS accounts_proxy_fallback_origin_id_idx ON accounts (proxy_fallback_origin_id); diff --git a/frontend/src/api/admin/accounts.ts b/frontend/src/api/admin/accounts.ts index bb75e30251..f800b940ba 100644 --- a/frontend/src/api/admin/accounts.ts +++ b/frontend/src/api/admin/accounts.ts @@ -659,6 +659,16 @@ export interface BatchOperationResult { warnings?: Array<{ account_id: number; warning: string }> } +/** + * Revert account proxy to original before fallback + * @param id - Account ID + * @returns Success confirmation + */ +export async function revertProxyFallback(id: number): Promise<{ message: string }> { + const { data } = await apiClient.post<{ message: string }>(`/admin/accounts/${id}/revert-proxy-fallback`) + return data +} + /** * Batch clear account errors * @param accountIds - Array of account IDs @@ -735,7 +745,8 @@ export const accountsAPI = { getAntigravityDefaultModelMapping, batchClearError, batchRefresh, - setPrivacy + setPrivacy, + revertProxyFallback } export default accountsAPI diff --git a/frontend/src/api/admin/proxies.ts b/frontend/src/api/admin/proxies.ts index 3e041ba9a5..d172bdfff5 100644 --- a/frontend/src/api/admin/proxies.ts +++ b/frontend/src/api/admin/proxies.ts @@ -27,7 +27,7 @@ export async function list( pageSize: number = 20, filters?: { protocol?: string - status?: 'active' | 'inactive' + status?: 'active' | 'inactive' | 'expired' search?: string sort_by?: string sort_order?: 'asc' | 'desc' @@ -227,7 +227,7 @@ export async function exportData(options?: { ids?: number[] filters?: { protocol?: string - status?: 'active' | 'inactive' + status?: 'active' | 'inactive' | 'expired' search?: string sort_by?: string sort_order?: 'asc' | 'desc' diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index d58b999005..91eb5e0378 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -3300,6 +3300,11 @@ export default { recoverStateHint: 'Used to recover error, rate-limit, and temporary unschedulable runtime state.', recoverStateSuccess: 'Account state recovered successfully', recoverStateFailed: 'Failed to recover account state', + fallbackActive: 'Fallback', + fallbackActiveTip: 'Origin proxy {origin} expired', + revertProxy: 'Revert proxy', + revertProxySuccess: 'Successfully reverted to original proxy', + revertProxyFailed: 'Failed to revert proxy', resetStatus: 'Reset Status', statusReset: 'Account status reset successfully', failedToResetStatus: 'Failed to reset account status', @@ -4176,6 +4181,8 @@ export default { status: 'Status', accounts: 'Accounts', latency: 'Latency', + expiry: 'Validity', + createdAt: 'Created', actions: 'Actions' }, testConnection: 'Test Connection', @@ -4273,7 +4280,21 @@ export default { hostRequired: 'Please enter host address', portInvalid: 'Port must be between 1-65535', deleteConfirm: - "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed." + "Are you sure you want to delete '{name}'? Accounts using this proxy will have their proxy removed.", + neverExpires: 'Never', + expired: 'Expired', + overdueDays: 'Overdue {days}d', + expiringInDays: 'Expires in {days}d', + remainingDays: '{days}d left', + expiresAt: 'Validity', + nDays: '{days}d', + expiryDaysPlaceholder: 'Custom days, empty = never', + expiryWarnDays: 'Expiry warning (days)', + fallbackMode: 'Failure fallback', + fallbackNone: 'No fallback', + fallbackProxy: 'Backup proxy', + fallbackDirect: 'Direct connection', + backupProxy: 'Backup proxy', }, // Redeem Codes diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index ea4339c3cb..9300d28a3b 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -3446,6 +3446,11 @@ export default { recoverStateHint: '用于恢复错误、限流和临时不可调度等可恢复状态。', recoverStateSuccess: '账号状态已恢复', recoverStateFailed: '恢复账号状态失败', + fallbackActive: '已回退', + fallbackActiveTip: '原代理 {origin} 已到期,当前使用备用代理', + revertProxy: '切回原代理', + revertProxySuccess: '已成功切回原代理', + revertProxyFailed: '切回原代理失败', resetStatus: '重置状态', statusReset: '账号状态已重置', failedToResetStatus: '重置账号状态失败', @@ -4259,6 +4264,8 @@ export default { status: '状态', accounts: '账号数', latency: '延迟', + expiry: '有效期', + createdAt: '创建时间', actions: '操作', nameLabel: '名称', namePlaceholder: '请输入代理名称', @@ -4394,7 +4401,21 @@ export default { nameRequired: '请输入代理名称', hostRequired: '请输入主机地址', portInvalid: '端口必须在 1-65535 之间', - deleteConfirm: "确定要删除代理 '{name}' 吗?使用此代理的账号将被移除代理设置。" + deleteConfirm: "确定要删除代理 '{name}' 吗?使用此代理的账号将被移除代理设置。", + neverExpires: '永不过期', + expired: '已过期', + overdueDays: '已超期 {days} 天', + expiringInDays: '{days} 天后到期', + remainingDays: '剩余 {days} 天', + expiresAt: '有效期', + nDays: '{days} 天', + expiryDaysPlaceholder: '自定义天数,留空 = 永不过期', + expiryWarnDays: '到期提醒提前天数', + fallbackMode: '失败回退', + fallbackNone: '不回退', + fallbackProxy: '指定备用代理', + fallbackDirect: '回退直连', + backupProxy: '备用代理', }, // Redeem Codes Management diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index ea12e8e757..3fa9e0f573 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -711,7 +711,7 @@ export interface Proxy { port: number username: string | null password?: string | null - status: 'active' | 'inactive' + status: 'active' | 'inactive' | 'expired' account_count?: number // Number of accounts using this proxy latency_ms?: number latency_status?: 'success' | 'failed' @@ -726,6 +726,10 @@ export interface Proxy { quality_grade?: string quality_summary?: string quality_checked?: number + expires_at: string | null + fallback_mode: 'none' | 'proxy' | 'direct' + backup_proxy_id?: number | null + expiry_warn_days: number created_at: string updated_at: string } @@ -831,6 +835,8 @@ export interface Account { antigravity_credits_overages?: Record } & Record) proxy_id: number | null + proxy_fallback_origin_id?: number | null + proxy_fallback_origin_name?: string | null concurrency: number load_factor?: number | null current_concurrency?: number // Real-time concurrency count from Redis @@ -1079,6 +1085,10 @@ export interface CreateProxyRequest { port: number username?: string | null password?: string | null + expires_at?: number | null // unix 秒;null/0 = 永不过期 + fallback_mode?: 'none' | 'proxy' | 'direct' + backup_proxy_id?: number | null + expiry_warn_days?: number } export interface UpdateProxyRequest { @@ -1089,6 +1099,10 @@ export interface UpdateProxyRequest { username?: string | null password?: string | null status?: 'active' | 'inactive' + expires_at?: number | null // unix 秒;null/0 = 永不过期 + fallback_mode?: 'none' | 'proxy' | 'direct' + backup_proxy_id?: number | null + expiry_warn_days?: number } export interface AdminDataPayload { diff --git a/frontend/src/utils/__tests__/proxyExpiry.spec.ts b/frontend/src/utils/__tests__/proxyExpiry.spec.ts new file mode 100644 index 0000000000..dc7c0b63f7 --- /dev/null +++ b/frontend/src/utils/__tests__/proxyExpiry.spec.ts @@ -0,0 +1,62 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { daysUntil, proxyExpiryBadgeClass, proxyExpiryLabelKey } from '../proxyExpiry' + +// 固定「现在」,按天数构造确定输入:isoInDays(n) 距今正好 n 天 +const NOW = new Date('2026-06-02T00:00:00Z') +const isoInDays = (n: number): string => new Date(NOW.getTime() + n * 86400000).toISOString() + +beforeEach(() => { + vi.useFakeTimers() + vi.setSystemTime(NOW) +}) +afterEach(() => { + vi.useRealTimers() +}) + +describe('daysUntil', () => { + it('返回距今整天数', () => { + expect(daysUntil(isoInDays(10))).toBe(10) + expect(daysUntil(isoInDays(-3))).toBe(-3) + }) +}) + +describe('proxyExpiryBadgeClass', () => { + it('status=expired → danger', () => { + expect(proxyExpiryBadgeClass(isoInDays(30), 'expired')).toBe('badge badge-danger') + }) + it('≤3 天 → danger(含边界 3)', () => { + expect(proxyExpiryBadgeClass(isoInDays(2), 'active')).toBe('badge badge-danger') + expect(proxyExpiryBadgeClass(isoInDays(3), 'active')).toBe('badge badge-danger') + }) + it('4–7 天 → warning(含边界 7)', () => { + expect(proxyExpiryBadgeClass(isoInDays(5), 'active')).toBe('badge badge-warning') + expect(proxyExpiryBadgeClass(isoInDays(7), 'active')).toBe('badge badge-warning') + }) + it('>7 天 → gray', () => { + expect(proxyExpiryBadgeClass(isoInDays(30), 'active')).toBe('text-gray-500') + }) +}) + +describe('proxyExpiryLabelKey', () => { + it('status=expired → expired key', () => { + expect(proxyExpiryLabelKey(isoInDays(30), 'expired')).toEqual({ key: 'admin.proxies.expired' }) + }) + it('已逾期(d<0) → overdueDays', () => { + expect(proxyExpiryLabelKey(isoInDays(-3), 'active')).toEqual({ + key: 'admin.proxies.overdueDays', + params: { days: 3 }, + }) + }) + it('≤7 天 → expiringInDays', () => { + expect(proxyExpiryLabelKey(isoInDays(5), 'active')).toEqual({ + key: 'admin.proxies.expiringInDays', + params: { days: 5 }, + }) + }) + it('>7 天 → remainingDays', () => { + expect(proxyExpiryLabelKey(isoInDays(30), 'active')).toEqual({ + key: 'admin.proxies.remainingDays', + params: { days: 30 }, + }) + }) +}) diff --git a/frontend/src/utils/proxyExpiry.ts b/frontend/src/utils/proxyExpiry.ts new file mode 100644 index 0000000000..610c4cbeac --- /dev/null +++ b/frontend/src/utils/proxyExpiry.ts @@ -0,0 +1,29 @@ +// 代理有效期展示逻辑(ProxiesView 与 AccountsView 共用)。 +// 到期紧迫度固定两档:剩余 ≤3 天红、≤7 天黄(不读 per-proxy expiry_warn_days)。 +export const EXPIRY_WARN_DAYS = 7 +export const EXPIRY_DANGER_DAYS = 3 + +// 距今整天数(向上取整)。 +export const daysUntil = (iso: string): number => + Math.ceil((new Date(iso).getTime() - Date.now()) / 86400000) + +// 倒计时徽章的 CSS class(纯函数,无 i18n 依赖)。 +export function proxyExpiryBadgeClass(expiresAt: string | null, status?: string): string { + if (status === 'expired') return 'badge badge-danger' + const d = expiresAt ? daysUntil(expiresAt) : Infinity + if (d <= EXPIRY_DANGER_DAYS) return 'badge badge-danger' + if (d <= EXPIRY_WARN_DAYS) return 'badge badge-warning' + return 'text-gray-500' +} + +// 倒计时文案的 i18n key + 参数(返回 key 而非已翻译文本,便于单测且不耦合 i18n)。 +export function proxyExpiryLabelKey( + expiresAt: string | null, + status?: string, +): { key: string; params?: { days: number } } { + if (status === 'expired') return { key: 'admin.proxies.expired' } + const d = expiresAt ? daysUntil(expiresAt) : Infinity + if (d < 0) return { key: 'admin.proxies.overdueDays', params: { days: Math.abs(d) } } + if (d <= EXPIRY_WARN_DAYS) return { key: 'admin.proxies.expiringInDays', params: { days: d } } + return { key: 'admin.proxies.remainingDays', params: { days: d } } +} diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 04b46a8de0..8b5b840fc4 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -288,13 +288,25 @@ /> + + + + @@ -475,6 +492,41 @@ +
+ +
+ +
+ + +
+
+ + +
@@ -673,6 +725,41 @@ + + +
+ + +
@@ -899,6 +986,8 @@ import { useClipboard } from '@/composables/useClipboard' import { useSwipeSelect } from '@/composables/useSwipeSelect' import { useTableSelection } from '@/composables/useTableSelection' import { getPersistedPageSize } from '@/composables/usePersistedPageSize' +import { formatDateTime } from '@/utils/format' +import { proxyExpiryBadgeClass, proxyExpiryLabelKey } from '@/utils/proxyExpiry' const { t } = useI18n() const appStore = useAppStore() @@ -913,6 +1002,8 @@ const columns = computed(() => [ { key: 'location', label: t('admin.proxies.columns.location'), sortable: false }, { key: 'account_count', label: t('admin.proxies.columns.accounts'), sortable: true }, { key: 'latency', label: t('admin.proxies.columns.latency'), sortable: false }, + { key: 'expiry', label: t('admin.proxies.columns.expiry'), sortable: true }, + { key: 'created_at', label: t('admin.proxies.columns.createdAt'), sortable: true }, { key: 'status', label: t('admin.proxies.columns.status'), sortable: true }, { key: 'actions', label: t('admin.proxies.columns.actions'), sortable: false } ]) @@ -929,7 +1020,8 @@ const protocolOptions = computed(() => [ const statusOptions = computed(() => [ { value: '', label: t('admin.proxies.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, - { value: 'inactive', label: t('admin.accounts.status.inactive') } + { value: 'inactive', label: t('admin.accounts.status.inactive') }, + { value: 'expired', label: t('admin.proxies.expired') } ]) // Form options @@ -1035,7 +1127,11 @@ const createForm = reactive({ host: '', port: 8080, username: '', - password: '' + password: '', + expires_at: '' as string, + fallback_mode: 'none' as 'none' | 'proxy' | 'direct', + backup_proxy_id: null as number | null, + expiry_warn_days: 7 as number, }) const editForm = reactive({ @@ -1045,9 +1141,22 @@ const editForm = reactive({ port: 8080, username: '', password: '', - status: 'active' as 'active' | 'inactive' + status: 'active' as 'active' | 'inactive' | 'expired', + expires_at: '' as string, + fallback_mode: 'none' as 'none' | 'proxy' | 'direct', + backup_proxy_id: null as number | null, + expiry_warn_days: 7 as number, }) +const allProxiesForBackup = ref([]) +const loadBackupProxyOptions = async () => { + allProxiesForBackup.value = await adminAPI.proxies.getAllWithCount() +} +const backupProxyOptions = (excludeId?: number) => + allProxiesForBackup.value + .filter(p => p.id !== excludeId) + .map(p => ({ label: `${p.name} (${p.host}:${p.port})`, value: p.id })) + let abortController: AbortController | null = null const isAbortError = (error: unknown) => { @@ -1072,7 +1181,7 @@ const toggleSelectAllVisible = (event: Event) => { const buildProxyQueryFilters = () => ({ protocol: filters.protocol || undefined, - status: (filters.status || undefined) as 'active' | 'inactive' | undefined, + status: (filters.status || undefined) as 'active' | 'inactive' | 'expired' | undefined, search: searchQuery.value || undefined, sort_by: sortState.sort_by, sort_order: sortState.sort_order @@ -1148,6 +1257,10 @@ const closeCreateModal = () => { createForm.port = 8080 createForm.username = '' createForm.password = '' + createForm.expires_at = '' + createForm.fallback_mode = 'none' + createForm.backup_proxy_id = null + createForm.expiry_warn_days = 7 createPasswordVisible.value = false batchInput.value = '' batchParseResult.total = 0 @@ -1272,7 +1385,11 @@ const handleCreateProxy = async () => { host: createForm.host.trim(), port: createForm.port, username: createForm.username.trim() || null, - password: createForm.password.trim() || null + password: createForm.password.trim() || null, + expires_at: createForm.expires_at ? Math.floor(new Date(createForm.expires_at).getTime() / 1000) : null, + fallback_mode: createForm.fallback_mode, + backup_proxy_id: createForm.fallback_mode === 'proxy' ? createForm.backup_proxy_id : null, + expiry_warn_days: createForm.expiry_warn_days, }) appStore.showSuccess(t('admin.proxies.proxyCreated')) closeCreateModal() @@ -1293,7 +1410,11 @@ const handleEdit = (proxy: Proxy) => { editForm.port = proxy.port editForm.username = proxy.username || '' editForm.password = proxy.password || '' - editForm.status = proxy.status + editForm.status = proxy.status === 'expired' ? 'inactive' : proxy.status + editForm.expires_at = proxy.expires_at ? proxy.expires_at.slice(0, 10) : '' + editForm.fallback_mode = proxy.fallback_mode || 'none' + editForm.backup_proxy_id = proxy.backup_proxy_id ?? null + editForm.expiry_warn_days = proxy.expiry_warn_days ?? 7 editPasswordVisible.value = false editPasswordDirty.value = false showEditModal.value = true @@ -1329,7 +1450,11 @@ const handleUpdateProxy = async () => { host: editForm.host.trim(), port: editForm.port, username: editForm.username.trim() || null, - status: editForm.status + status: editForm.status, + expires_at: editForm.expires_at ? Math.floor(new Date(editForm.expires_at).getTime() / 1000) : null, + fallback_mode: editForm.fallback_mode, + backup_proxy_id: editForm.fallback_mode === 'proxy' ? editForm.backup_proxy_id : null, + expiry_warn_days: editForm.expiry_warn_days, } // Only include password if user actually modified the field @@ -1575,6 +1700,59 @@ const qualityStatusLabel = (status: string) => { return t('admin.proxies.qualityStatusFail') } +// 有效期「选天数」⇄ 日历联动:天数自 base 起算(创建=今天;编辑=代理创建日),本地日历日 round-trip 稳定;canonical 仍是 expires_at 日期串 +const EXPIRY_PRESETS = [7, 30, 90, 180] +const toLocalDateStr = (dt: Date): string => { + const y = dt.getFullYear() + const m = String(dt.getMonth() + 1).padStart(2, '0') + const d = String(dt.getDate()).padStart(2, '0') + return `${y}-${m}-${d}` +} +// base 为空 → 今天本地 00:00;否则该日期本地 00:00 +const baseDateOrToday = (baseDateStr: string): Date => { + const base = baseDateStr ? new Date(`${baseDateStr}T00:00:00`) : new Date() + base.setHours(0, 0, 0, 0) + return base +} +// base + N 天 → 本地 YYYY-MM-DD;N≤0/空 → '' 表示永不过期 +const addDaysToBase = (baseDateStr: string, n: number | null): string => { + const days = Number(n) + if (!days || days <= 0) return '' + const dt = baseDateOrToday(baseDateStr) + dt.setDate(dt.getDate() + days) + return toLocalDateStr(dt) +} +// target 相对 base 的整天数(本地日历差,避免时区/时刻抖动) +const daysFromBase = (baseDateStr: string, targetDateStr: string): number | null => { + if (!targetDateStr) return null + const target = new Date(`${targetDateStr}T00:00:00`) + return Math.round((target.getTime() - baseDateOrToday(baseDateStr).getTime()) / 86400000) +} +// 编辑时有效期自「代理创建日」起算;创建时无 created_at → base='' 用今天 +const editBaseDate = computed(() => + editingProxy.value?.created_at ? editingProxy.value.created_at.slice(0, 10) : '', +) +const createExpiresDays = computed({ + get: () => daysFromBase('', createForm.expires_at), + set: (v) => { + createForm.expires_at = addDaysToBase('', v) + }, +}) +const editExpiresDays = computed({ + get: () => daysFromBase(editBaseDate.value, editForm.expires_at), + set: (v) => { + editForm.expires_at = addDaysToBase(editBaseDate.value, v) + }, +}) + +const expiryLabel = (row: Proxy): string => { + const { key, params } = proxyExpiryLabelKey(row.expires_at, row.status) + return params ? t(key, params) : t(key) +} + +const expiryBadgeClass = (row: Proxy): string => + proxyExpiryBadgeClass(row.expires_at, row.status) + const qualityOverallClass = (status?: string) => { if (status === 'healthy') return 'badge-success' if (status === 'warn') return 'badge-warning' @@ -1877,6 +2055,7 @@ function closeCopyMenu() { onMounted(() => { loadProxies() + loadBackupProxyOptions() document.addEventListener('click', closeCopyMenu) })