feat(proxies): 代理有效期与失败回退

- schema/迁移: 代理有效期、提醒天数、失败回退配置 + 账号 fallback 来源字段
- service/repo/DTO/handler: CRUD 透传新字段 + 校验
- fallback 目标解析纯函数(链式解析 + 环检测 + 兜底)
- SweepExpiredProxies 到期改投账号 + outbox 失效
- ProxyExpiryService 后台到期扫描任务 + wire 注册
- 账号侧手动回切原代理 + fallback 来源徽章/按钮
- 前端: 创建/编辑表单、列表到期徽章、类型/API/i18n
- ops 告警: proxy_expired_count / proxy_expiring_soon_count 指标
- 导入导出携带有效期/回退字段(备用按 name 映射)
- 补全测试 stub + 集成测试 + review 问题修复
This commit is contained in:
DaydreamCoding
2026-06-08 00:01:30 +08:00
parent f20e6bf769
commit af19d44327
61 changed files with 3430 additions and 281 deletions
+5
View File
@@ -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
+8 -2
View File
@@ -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
+2
View File
@@ -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,
+15 -1
View File
@@ -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(", ")
+8
View File
@@ -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()
+55
View File
@@ -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))
+98
View File
@@ -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) {
+72
View File
@@ -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)
}
+16
View File
@@ -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
+35 -11
View File
@@ -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",
}
+512 -72
View File
@@ -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)
}
+72 -4
View File
@@ -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()
}
+58
View File
@@ -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),
)
}
+228
View File
@@ -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...))
+329
View File
@@ -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 {
+90 -13
View File
@@ -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) {
+270
View File
@@ -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
+17 -7
View File
@@ -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.
+3
View File
@@ -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: 账户最大并发请求数
// 用于限制同一时间对该账户发起的请求数量
+17
View File
@@ -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"),
}
}
+126 -24
View File
@@ -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
}
@@ -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) {
@@ -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)
@@ -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{} {
+118 -16
View File
@@ -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,
+53 -26
View File
@@ -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)
+15 -9
View File
@@ -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,
}
}
+22 -15
View File
@@ -160,21 +160,23 @@ type Account struct {
Type string `json:"type"`
// Credentials 经 RedactCredentials 处理后只含非敏感子键;敏感 token / api_key / 私钥
// 的存在性通过 CredentialsStatus(has_<key>)暴露,原始值不返回前端。
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 {
@@ -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
}
@@ -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)
}
+183 -18
View File
@@ -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
}
@@ -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: 永不过期(置顶)→远期→快到期→过期",
)
}
@@ -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
}
+1
View File
@@ -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)
+12 -10
View File
@@ -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
+6 -2
View File
@@ -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.
@@ -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(账号不存在)
+74 -20
View File
@@ -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
@@ -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
@@ -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)
@@ -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)
@@ -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 {
@@ -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")
}
// =====================
// 测试用例
@@ -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{
+25 -10
View File
@@ -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,
@@ -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)
}
}
@@ -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
}
}
@@ -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)
})
}
@@ -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 创建代理请求
@@ -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 {
+10 -1
View File
@@ -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,
@@ -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);
+12 -1
View File
@@ -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
+2 -2
View File
@@ -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'
+22 -1
View File
@@ -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
+22 -1
View File
@@ -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
+15 -1
View File
@@ -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<string, { activated_at: string; active_until: string }>
} & Record<string, unknown>)
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 {
@@ -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 },
})
})
})
+29
View File
@@ -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 } }
}
+35 -6
View File
@@ -288,13 +288,25 @@
/>
</template>
<template #cell-proxy="{ row }">
<div v-if="row.proxy" class="flex items-center gap-2">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ row.proxy.name }}</span>
<span v-if="row.proxy.country_code" class="text-xs text-gray-500 dark:text-gray-400">
({{ row.proxy.country_code }})
</span>
<div class="flex flex-col gap-1">
<div v-if="row.proxy" class="flex items-center gap-2">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ row.proxy.name }}</span>
<span v-if="row.proxy.country_code" class="text-xs text-gray-500 dark:text-gray-400">
({{ row.proxy.country_code }})
</span>
</div>
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
<div v-if="row.proxy && row.proxy.expires_at" class="flex items-center gap-2 text-xs">
<span class="text-gray-600 dark:text-gray-300">{{ formatDateTime(row.proxy.expires_at) }}</span>
<span :class="proxyExpiryBadge(row.proxy)">{{ proxyExpiryText(row.proxy) }}</span>
</div>
<div v-if="row.proxy_fallback_origin_id" class="flex items-center gap-1">
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200" :title="t('admin.accounts.fallbackActiveTip', { origin: row.proxy_fallback_origin_name })">
{{ t('admin.accounts.fallbackActive') }}
</span>
<button class="text-xs px-1.5 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700" @click="onRevertFallback(row)">{{ t('admin.accounts.revertProxy') }}</button>
</div>
</div>
<span v-else class="text-sm text-gray-400 dark:text-dark-500">-</span>
</template>
<template #cell-rate_multiplier="{ row }">
<span class="text-sm font-mono text-gray-700 dark:text-gray-300">
@@ -421,6 +433,7 @@ import ErrorPassthroughRulesModal from '@/components/admin/ErrorPassthroughRules
import TLSFingerprintProfilesModal from '@/components/admin/TLSFingerprintProfilesModal.vue'
import { buildOpenAIUsageRefreshKey } from '@/utils/accountUsageRefresh'
import { formatDateTime, formatRelativeTime } from '@/utils/format'
import { proxyExpiryBadgeClass, proxyExpiryLabelKey } from '@/utils/proxyExpiry'
import type { Account, AccountPlatform, AccountType, Proxy as AccountProxy, AdminGroup, WindowStats, ClaudeModel } from '@/types'
const { t } = useI18n()
@@ -1597,6 +1610,16 @@ const handleSetPrivacy = async (a: Account) => {
appStore.showError(error?.response?.data?.message || t('admin.accounts.privacyFailed'))
}
}
const onRevertFallback = async (a: Account) => {
try {
await adminAPI.accounts.revertProxyFallback(a.id)
appStore.showSuccess(t('admin.accounts.revertProxySuccess'))
reload()
} catch (error: any) {
console.error('Failed to revert proxy fallback:', error)
appStore.showError(error?.response?.data?.message || t('admin.accounts.revertProxyFailed'))
}
}
const handleDelete = (a: Account) => { deletingAcc.value = a; showDeleteDialog.value = true }
const confirmDelete = async () => { if(!deletingAcc.value) return; try { await adminAPI.accounts.delete(deletingAcc.value.id); showDeleteDialog.value = false; deletingAcc.value = null; reload() } catch (error) { console.error('Failed to delete account:', error) } }
const handleToggleSchedulable = async (a: Account) => {
@@ -1639,6 +1662,12 @@ const isExpired = (value: number | null) => {
if (!value) return false
return value * 1000 <= Date.now()
}
// 所绑定代理的有效期(逻辑同 /admin/proxies,见 utils/proxyExpiry)
const proxyExpiryBadge = (p: AccountProxy): string => proxyExpiryBadgeClass(p.expires_at, p.status)
const proxyExpiryText = (p: AccountProxy): string => {
const { key, params } = proxyExpiryLabelKey(p.expires_at, p.status)
return params ? t(key, params) : t(key)
}
// 滚动时关闭操作菜单(不关闭列设置下拉菜单)
const handleScroll = () => {
+187 -8
View File
@@ -244,8 +244,25 @@
</div>
</template>
<template #cell-expiry="{ row }">
<span v-if="!row.expires_at" class="text-sm text-gray-400">{{ t('admin.proxies.neverExpires') }}</span>
<div v-else class="flex flex-col text-xs">
<span class="text-gray-700 dark:text-gray-200">{{ formatDateTime(row.expires_at) }}</span>
<span :class="expiryBadgeClass(row)">{{ expiryLabel(row) }}</span>
</div>
</template>
<template #cell-created_at="{ row }">
<span class="text-xs text-gray-600 dark:text-gray-300">{{ formatDateTime(row.created_at) }}</span>
</template>
<template #cell-status="{ value }">
<span :class="['badge', value === 'active' ? 'badge-success' : 'badge-danger']">
<span
:class="[
'badge',
value === 'active' ? 'badge-success' : value === 'expired' ? 'badge-danger' : 'badge-danger'
]"
>
{{ t('admin.accounts.status.' + value) }}
</span>
</template>
@@ -475,6 +492,41 @@
</button>
</div>
</div>
<div>
<label class="input-label">{{ t('admin.proxies.expiresAt') }}</label>
<div class="mb-2 flex flex-wrap gap-2">
<button
v-for="d in EXPIRY_PRESETS"
:key="d"
type="button"
class="btn btn-sm"
:class="createForm.expires_at === addDaysToBase('', d) ? 'btn-primary' : 'btn-secondary'"
@click="createExpiresDays = d"
>
{{ t('admin.proxies.nDays', { days: d }) }}
</button>
</div>
<input
v-model.number="createExpiresDays"
type="number"
min="0"
class="input mb-2"
:placeholder="t('admin.proxies.expiryDaysPlaceholder')"
/>
<input v-model="createForm.expires_at" type="date" class="input" />
</div>
<div>
<label class="input-label">{{ t('admin.proxies.fallbackMode') }}</label>
<Select v-model="createForm.fallback_mode" :options="[
{ label: t('admin.proxies.fallbackNone'), value: 'none' },
{ label: t('admin.proxies.fallbackProxy'), value: 'proxy' },
{ label: t('admin.proxies.fallbackDirect'), value: 'direct' },
]" />
</div>
<div v-if="createForm.fallback_mode === 'proxy'">
<label class="input-label">{{ t('admin.proxies.backupProxy') }}</label>
<Select v-model="createForm.backup_proxy_id" :options="backupProxyOptions()" />
</div>
</form>
@@ -673,6 +725,41 @@
<label class="input-label">{{ t('admin.proxies.status') }}</label>
<Select v-model="editForm.status" :options="editStatusOptions" />
</div>
<div>
<label class="input-label">{{ t('admin.proxies.expiresAt') }}</label>
<div class="mb-2 flex flex-wrap gap-2">
<button
v-for="d in EXPIRY_PRESETS"
:key="d"
type="button"
class="btn btn-sm"
:class="editForm.expires_at === addDaysToBase(editBaseDate, d) ? 'btn-primary' : 'btn-secondary'"
@click="editExpiresDays = d"
>
{{ t('admin.proxies.nDays', { days: d }) }}
</button>
</div>
<input
v-model.number="editExpiresDays"
type="number"
min="0"
class="input mb-2"
:placeholder="t('admin.proxies.expiryDaysPlaceholder')"
/>
<input v-model="editForm.expires_at" type="date" class="input" />
</div>
<div>
<label class="input-label">{{ t('admin.proxies.fallbackMode') }}</label>
<Select v-model="editForm.fallback_mode" :options="[
{ label: t('admin.proxies.fallbackNone'), value: 'none' },
{ label: t('admin.proxies.fallbackProxy'), value: 'proxy' },
{ label: t('admin.proxies.fallbackDirect'), value: 'direct' },
]" />
</div>
<div v-if="editForm.fallback_mode === 'proxy'">
<label class="input-label">{{ t('admin.proxies.backupProxy') }}</label>
<Select v-model="editForm.backup_proxy_id" :options="backupProxyOptions(editingProxy?.id)" />
</div>
</form>
@@ -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<Column[]>(() => [
{ 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<Proxy[]>([])
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<number | null>({
get: () => daysFromBase('', createForm.expires_at),
set: (v) => {
createForm.expires_at = addDaysToBase('', v)
},
})
const editExpiresDays = computed<number | null>({
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)
})