feat: auto-pause expired accounts
This commit is contained in:
@@ -63,6 +63,7 @@ func provideCleanup(
|
|||||||
entClient *ent.Client,
|
entClient *ent.Client,
|
||||||
rdb *redis.Client,
|
rdb *redis.Client,
|
||||||
tokenRefresh *service.TokenRefreshService,
|
tokenRefresh *service.TokenRefreshService,
|
||||||
|
accountExpiry *service.AccountExpiryService,
|
||||||
pricing *service.PricingService,
|
pricing *service.PricingService,
|
||||||
emailQueue *service.EmailQueueService,
|
emailQueue *service.EmailQueueService,
|
||||||
billingCache *service.BillingCacheService,
|
billingCache *service.BillingCacheService,
|
||||||
@@ -84,6 +85,10 @@ func provideCleanup(
|
|||||||
tokenRefresh.Stop()
|
tokenRefresh.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AccountExpiryService", func() error {
|
||||||
|
accountExpiry.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"PricingService", func() error {
|
{"PricingService", func() error {
|
||||||
pricing.Stop()
|
pricing.Stop()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
geminiOAuthClient := repository.NewGeminiOAuthClient(configConfig)
|
||||||
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
|
geminiCliCodeAssistClient := repository.NewGeminiCliCodeAssistClient()
|
||||||
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
|
geminiOAuthService := service.NewGeminiOAuthService(proxyRepository, geminiOAuthClient, geminiCliCodeAssistClient, configConfig)
|
||||||
|
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
||||||
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
|
geminiQuotaService := service.NewGeminiQuotaService(configConfig, settingRepository)
|
||||||
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
|
tempUnschedCache := repository.NewTempUnschedCache(redisClient)
|
||||||
rateLimitService := service.NewRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache)
|
rateLimitService := service.NewRateLimitService(accountRepository, usageLogRepository, configConfig, geminiQuotaService, tempUnschedCache)
|
||||||
@@ -97,13 +98,12 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
|
geminiTokenCache := repository.NewGeminiTokenCache(redisClient)
|
||||||
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
|
geminiTokenProvider := service.NewGeminiTokenProvider(accountRepository, geminiTokenCache, geminiOAuthService)
|
||||||
gatewayCache := repository.NewGatewayCache(redisClient)
|
gatewayCache := repository.NewGatewayCache(redisClient)
|
||||||
antigravityOAuthService := service.NewAntigravityOAuthService(proxyRepository)
|
|
||||||
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
|
antigravityTokenProvider := service.NewAntigravityTokenProvider(accountRepository, geminiTokenCache, antigravityOAuthService)
|
||||||
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
httpUpstream := repository.NewHTTPUpstream(configConfig)
|
||||||
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
antigravityGatewayService := service.NewAntigravityGatewayService(accountRepository, gatewayCache, antigravityTokenProvider, rateLimitService, httpUpstream, settingService)
|
||||||
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
|
accountTestService := service.NewAccountTestService(accountRepository, geminiTokenProvider, antigravityGatewayService, httpUpstream, configConfig)
|
||||||
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig)
|
||||||
concurrencyService := service.NewConcurrencyService(concurrencyCache)
|
concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig)
|
||||||
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
crsSyncService := service.NewCRSSyncService(accountRepository, proxyRepository, oAuthService, openAIOAuthService, geminiOAuthService, configConfig)
|
||||||
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
|
accountHandler := admin.NewAccountHandler(adminService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, rateLimitService, accountUsageService, accountTestService, concurrencyService, crsSyncService)
|
||||||
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
oAuthHandler := admin.NewOAuthHandler(oAuthService)
|
||||||
@@ -148,7 +148,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) {
|
|||||||
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
engine := server.ProvideRouter(configConfig, handlers, jwtAuthMiddleware, adminAuthMiddleware, apiKeyAuthMiddleware, apiKeyService, subscriptionService)
|
||||||
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
httpServer := server.ProvideHTTPServer(configConfig, engine)
|
||||||
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, configConfig)
|
tokenRefreshService := service.ProvideTokenRefreshService(accountRepository, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService, configConfig)
|
||||||
v := provideCleanup(client, redisClient, tokenRefreshService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
accountExpiryService := service.ProvideAccountExpiryService(accountRepository)
|
||||||
|
v := provideCleanup(client, redisClient, tokenRefreshService, accountExpiryService, pricingService, emailQueueService, billingCacheService, oAuthService, openAIOAuthService, geminiOAuthService, antigravityOAuthService)
|
||||||
application := &Application{
|
application := &Application{
|
||||||
Server: httpServer,
|
Server: httpServer,
|
||||||
Cleanup: v,
|
Cleanup: v,
|
||||||
@@ -174,6 +175,7 @@ func provideCleanup(
|
|||||||
entClient *ent.Client,
|
entClient *ent.Client,
|
||||||
rdb *redis.Client,
|
rdb *redis.Client,
|
||||||
tokenRefresh *service.TokenRefreshService,
|
tokenRefresh *service.TokenRefreshService,
|
||||||
|
accountExpiry *service.AccountExpiryService,
|
||||||
pricing *service.PricingService,
|
pricing *service.PricingService,
|
||||||
emailQueue *service.EmailQueueService,
|
emailQueue *service.EmailQueueService,
|
||||||
billingCache *service.BillingCacheService,
|
billingCache *service.BillingCacheService,
|
||||||
@@ -194,6 +196,10 @@ func provideCleanup(
|
|||||||
tokenRefresh.Stop()
|
tokenRefresh.Stop()
|
||||||
return nil
|
return nil
|
||||||
}},
|
}},
|
||||||
|
{"AccountExpiryService", func() error {
|
||||||
|
accountExpiry.Stop()
|
||||||
|
return nil
|
||||||
|
}},
|
||||||
{"PricingService", func() error {
|
{"PricingService", func() error {
|
||||||
pricing.Stop()
|
pricing.Stop()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ type Account struct {
|
|||||||
ErrorMessage *string `json:"error_message,omitempty"`
|
ErrorMessage *string `json:"error_message,omitempty"`
|
||||||
// LastUsedAt holds the value of the "last_used_at" field.
|
// LastUsedAt holds the value of the "last_used_at" field.
|
||||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||||
|
// Account expiration time (NULL means no expiration).
|
||||||
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||||
|
// Auto pause scheduling when account expires.
|
||||||
|
AutoPauseOnExpired bool `json:"auto_pause_on_expired,omitempty"`
|
||||||
// Schedulable holds the value of the "schedulable" field.
|
// Schedulable holds the value of the "schedulable" field.
|
||||||
Schedulable bool `json:"schedulable,omitempty"`
|
Schedulable bool `json:"schedulable,omitempty"`
|
||||||
// RateLimitedAt holds the value of the "rate_limited_at" field.
|
// RateLimitedAt holds the value of the "rate_limited_at" field.
|
||||||
@@ -129,13 +133,13 @@ func (*Account) scanValues(columns []string) ([]any, error) {
|
|||||||
switch columns[i] {
|
switch columns[i] {
|
||||||
case account.FieldCredentials, account.FieldExtra:
|
case account.FieldCredentials, account.FieldExtra:
|
||||||
values[i] = new([]byte)
|
values[i] = new([]byte)
|
||||||
case account.FieldSchedulable:
|
case account.FieldAutoPauseOnExpired, account.FieldSchedulable:
|
||||||
values[i] = new(sql.NullBool)
|
values[i] = new(sql.NullBool)
|
||||||
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
|
case account.FieldID, account.FieldProxyID, account.FieldConcurrency, account.FieldPriority:
|
||||||
values[i] = new(sql.NullInt64)
|
values[i] = new(sql.NullInt64)
|
||||||
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldSessionWindowStatus:
|
case account.FieldName, account.FieldNotes, account.FieldPlatform, account.FieldType, account.FieldStatus, account.FieldErrorMessage, account.FieldSessionWindowStatus:
|
||||||
values[i] = new(sql.NullString)
|
values[i] = new(sql.NullString)
|
||||||
case account.FieldCreatedAt, account.FieldUpdatedAt, account.FieldDeletedAt, account.FieldLastUsedAt, account.FieldRateLimitedAt, account.FieldRateLimitResetAt, account.FieldOverloadUntil, account.FieldSessionWindowStart, account.FieldSessionWindowEnd:
|
case account.FieldCreatedAt, account.FieldUpdatedAt, account.FieldDeletedAt, account.FieldLastUsedAt, account.FieldExpiresAt, account.FieldRateLimitedAt, account.FieldRateLimitResetAt, account.FieldOverloadUntil, account.FieldSessionWindowStart, account.FieldSessionWindowEnd:
|
||||||
values[i] = new(sql.NullTime)
|
values[i] = new(sql.NullTime)
|
||||||
default:
|
default:
|
||||||
values[i] = new(sql.UnknownType)
|
values[i] = new(sql.UnknownType)
|
||||||
@@ -257,6 +261,19 @@ func (_m *Account) assignValues(columns []string, values []any) error {
|
|||||||
_m.LastUsedAt = new(time.Time)
|
_m.LastUsedAt = new(time.Time)
|
||||||
*_m.LastUsedAt = value.Time
|
*_m.LastUsedAt = value.Time
|
||||||
}
|
}
|
||||||
|
case account.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 account.FieldAutoPauseOnExpired:
|
||||||
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field auto_pause_on_expired", values[i])
|
||||||
|
} else if value.Valid {
|
||||||
|
_m.AutoPauseOnExpired = value.Bool
|
||||||
|
}
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
if value, ok := values[i].(*sql.NullBool); !ok {
|
if value, ok := values[i].(*sql.NullBool); !ok {
|
||||||
return fmt.Errorf("unexpected type %T for field schedulable", values[i])
|
return fmt.Errorf("unexpected type %T for field schedulable", values[i])
|
||||||
@@ -416,6 +433,14 @@ func (_m *Account) String() string {
|
|||||||
builder.WriteString(v.Format(time.ANSIC))
|
builder.WriteString(v.Format(time.ANSIC))
|
||||||
}
|
}
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
if v := _m.ExpiresAt; v != nil {
|
||||||
|
builder.WriteString("expires_at=")
|
||||||
|
builder.WriteString(v.Format(time.ANSIC))
|
||||||
|
}
|
||||||
|
builder.WriteString(", ")
|
||||||
|
builder.WriteString("auto_pause_on_expired=")
|
||||||
|
builder.WriteString(fmt.Sprintf("%v", _m.AutoPauseOnExpired))
|
||||||
|
builder.WriteString(", ")
|
||||||
builder.WriteString("schedulable=")
|
builder.WriteString("schedulable=")
|
||||||
builder.WriteString(fmt.Sprintf("%v", _m.Schedulable))
|
builder.WriteString(fmt.Sprintf("%v", _m.Schedulable))
|
||||||
builder.WriteString(", ")
|
builder.WriteString(", ")
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ const (
|
|||||||
FieldErrorMessage = "error_message"
|
FieldErrorMessage = "error_message"
|
||||||
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
|
// FieldLastUsedAt holds the string denoting the last_used_at field in the database.
|
||||||
FieldLastUsedAt = "last_used_at"
|
FieldLastUsedAt = "last_used_at"
|
||||||
|
// FieldExpiresAt holds the string denoting the expires_at field in the database.
|
||||||
|
FieldExpiresAt = "expires_at"
|
||||||
|
// FieldAutoPauseOnExpired holds the string denoting the auto_pause_on_expired field in the database.
|
||||||
|
FieldAutoPauseOnExpired = "auto_pause_on_expired"
|
||||||
// FieldSchedulable holds the string denoting the schedulable field in the database.
|
// FieldSchedulable holds the string denoting the schedulable field in the database.
|
||||||
FieldSchedulable = "schedulable"
|
FieldSchedulable = "schedulable"
|
||||||
// FieldRateLimitedAt holds the string denoting the rate_limited_at field in the database.
|
// FieldRateLimitedAt holds the string denoting the rate_limited_at field in the database.
|
||||||
@@ -115,6 +119,8 @@ var Columns = []string{
|
|||||||
FieldStatus,
|
FieldStatus,
|
||||||
FieldErrorMessage,
|
FieldErrorMessage,
|
||||||
FieldLastUsedAt,
|
FieldLastUsedAt,
|
||||||
|
FieldExpiresAt,
|
||||||
|
FieldAutoPauseOnExpired,
|
||||||
FieldSchedulable,
|
FieldSchedulable,
|
||||||
FieldRateLimitedAt,
|
FieldRateLimitedAt,
|
||||||
FieldRateLimitResetAt,
|
FieldRateLimitResetAt,
|
||||||
@@ -172,6 +178,8 @@ var (
|
|||||||
DefaultStatus string
|
DefaultStatus string
|
||||||
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
// StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
||||||
StatusValidator func(string) error
|
StatusValidator func(string) error
|
||||||
|
// DefaultAutoPauseOnExpired holds the default value on creation for the "auto_pause_on_expired" field.
|
||||||
|
DefaultAutoPauseOnExpired bool
|
||||||
// DefaultSchedulable holds the default value on creation for the "schedulable" field.
|
// DefaultSchedulable holds the default value on creation for the "schedulable" field.
|
||||||
DefaultSchedulable bool
|
DefaultSchedulable bool
|
||||||
// SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
// SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
||||||
@@ -251,6 +259,16 @@ func ByLastUsedAt(opts ...sql.OrderTermOption) OrderOption {
|
|||||||
return sql.OrderByField(FieldLastUsedAt, opts...).ToFunc()
|
return sql.OrderByField(FieldLastUsedAt, opts...).ToFunc()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ByExpiresAt orders the results by the expires_at field.
|
||||||
|
func ByExpiresAt(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldExpiresAt, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByAutoPauseOnExpired orders the results by the auto_pause_on_expired field.
|
||||||
|
func ByAutoPauseOnExpired(opts ...sql.OrderTermOption) OrderOption {
|
||||||
|
return sql.OrderByField(FieldAutoPauseOnExpired, opts...).ToFunc()
|
||||||
|
}
|
||||||
|
|
||||||
// BySchedulable orders the results by the schedulable field.
|
// BySchedulable orders the results by the schedulable field.
|
||||||
func BySchedulable(opts ...sql.OrderTermOption) OrderOption {
|
func BySchedulable(opts ...sql.OrderTermOption) OrderOption {
|
||||||
return sql.OrderByField(FieldSchedulable, opts...).ToFunc()
|
return sql.OrderByField(FieldSchedulable, opts...).ToFunc()
|
||||||
|
|||||||
@@ -120,6 +120,16 @@ func LastUsedAt(v time.Time) predicate.Account {
|
|||||||
return predicate.Account(sql.FieldEQ(FieldLastUsedAt, v))
|
return predicate.Account(sql.FieldEQ(FieldLastUsedAt, v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpiresAt applies equality check predicate on the "expires_at" field. It's identical to ExpiresAtEQ.
|
||||||
|
func ExpiresAt(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpired applies equality check predicate on the "auto_pause_on_expired" field. It's identical to AutoPauseOnExpiredEQ.
|
||||||
|
func AutoPauseOnExpired(v bool) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldAutoPauseOnExpired, v))
|
||||||
|
}
|
||||||
|
|
||||||
// Schedulable applies equality check predicate on the "schedulable" field. It's identical to SchedulableEQ.
|
// Schedulable applies equality check predicate on the "schedulable" field. It's identical to SchedulableEQ.
|
||||||
func Schedulable(v bool) predicate.Account {
|
func Schedulable(v bool) predicate.Account {
|
||||||
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
||||||
@@ -855,6 +865,66 @@ func LastUsedAtNotNil() predicate.Account {
|
|||||||
return predicate.Account(sql.FieldNotNull(FieldLastUsedAt))
|
return predicate.Account(sql.FieldNotNull(FieldLastUsedAt))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExpiresAtEQ applies the EQ predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtEQ(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtNEQ applies the NEQ predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtNEQ(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNEQ(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtIn applies the In predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtIn(vs ...time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldIn(FieldExpiresAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtNotIn applies the NotIn predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtNotIn(vs ...time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNotIn(FieldExpiresAt, vs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtGT applies the GT predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtGT(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldGT(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtGTE applies the GTE predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtGTE(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldGTE(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtLT applies the LT predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtLT(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldLT(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtLTE applies the LTE predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtLTE(v time.Time) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldLTE(FieldExpiresAt, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtIsNil applies the IsNil predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtIsNil() predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldIsNull(FieldExpiresAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtNotNil applies the NotNil predicate on the "expires_at" field.
|
||||||
|
func ExpiresAtNotNil() predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNotNull(FieldExpiresAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpiredEQ applies the EQ predicate on the "auto_pause_on_expired" field.
|
||||||
|
func AutoPauseOnExpiredEQ(v bool) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldEQ(FieldAutoPauseOnExpired, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpiredNEQ applies the NEQ predicate on the "auto_pause_on_expired" field.
|
||||||
|
func AutoPauseOnExpiredNEQ(v bool) predicate.Account {
|
||||||
|
return predicate.Account(sql.FieldNEQ(FieldAutoPauseOnExpired, v))
|
||||||
|
}
|
||||||
|
|
||||||
// SchedulableEQ applies the EQ predicate on the "schedulable" field.
|
// SchedulableEQ applies the EQ predicate on the "schedulable" field.
|
||||||
func SchedulableEQ(v bool) predicate.Account {
|
func SchedulableEQ(v bool) predicate.Account {
|
||||||
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
return predicate.Account(sql.FieldEQ(FieldSchedulable, v))
|
||||||
|
|||||||
@@ -195,6 +195,34 @@ func (_c *AccountCreate) SetNillableLastUsedAt(v *time.Time) *AccountCreate {
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (_c *AccountCreate) SetExpiresAt(v time.Time) *AccountCreate {
|
||||||
|
_c.mutation.SetExpiresAt(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
|
||||||
|
func (_c *AccountCreate) SetNillableExpiresAt(v *time.Time) *AccountCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetExpiresAt(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (_c *AccountCreate) SetAutoPauseOnExpired(v bool) *AccountCreate {
|
||||||
|
_c.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
|
||||||
|
func (_c *AccountCreate) SetNillableAutoPauseOnExpired(v *bool) *AccountCreate {
|
||||||
|
if v != nil {
|
||||||
|
_c.SetAutoPauseOnExpired(*v)
|
||||||
|
}
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (_c *AccountCreate) SetSchedulable(v bool) *AccountCreate {
|
func (_c *AccountCreate) SetSchedulable(v bool) *AccountCreate {
|
||||||
_c.mutation.SetSchedulable(v)
|
_c.mutation.SetSchedulable(v)
|
||||||
@@ -405,6 +433,10 @@ func (_c *AccountCreate) defaults() error {
|
|||||||
v := account.DefaultStatus
|
v := account.DefaultStatus
|
||||||
_c.mutation.SetStatus(v)
|
_c.mutation.SetStatus(v)
|
||||||
}
|
}
|
||||||
|
if _, ok := _c.mutation.AutoPauseOnExpired(); !ok {
|
||||||
|
v := account.DefaultAutoPauseOnExpired
|
||||||
|
_c.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
}
|
||||||
if _, ok := _c.mutation.Schedulable(); !ok {
|
if _, ok := _c.mutation.Schedulable(); !ok {
|
||||||
v := account.DefaultSchedulable
|
v := account.DefaultSchedulable
|
||||||
_c.mutation.SetSchedulable(v)
|
_c.mutation.SetSchedulable(v)
|
||||||
@@ -464,6 +496,9 @@ func (_c *AccountCreate) check() error {
|
|||||||
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Account.status": %w`, err)}
|
return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "Account.status": %w`, err)}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, ok := _c.mutation.AutoPauseOnExpired(); !ok {
|
||||||
|
return &ValidationError{Name: "auto_pause_on_expired", err: errors.New(`ent: missing required field "Account.auto_pause_on_expired"`)}
|
||||||
|
}
|
||||||
if _, ok := _c.mutation.Schedulable(); !ok {
|
if _, ok := _c.mutation.Schedulable(); !ok {
|
||||||
return &ValidationError{Name: "schedulable", err: errors.New(`ent: missing required field "Account.schedulable"`)}
|
return &ValidationError{Name: "schedulable", err: errors.New(`ent: missing required field "Account.schedulable"`)}
|
||||||
}
|
}
|
||||||
@@ -555,6 +590,14 @@ func (_c *AccountCreate) createSpec() (*Account, *sqlgraph.CreateSpec) {
|
|||||||
_spec.SetField(account.FieldLastUsedAt, field.TypeTime, value)
|
_spec.SetField(account.FieldLastUsedAt, field.TypeTime, value)
|
||||||
_node.LastUsedAt = &value
|
_node.LastUsedAt = &value
|
||||||
}
|
}
|
||||||
|
if value, ok := _c.mutation.ExpiresAt(); ok {
|
||||||
|
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
|
||||||
|
_node.ExpiresAt = &value
|
||||||
|
}
|
||||||
|
if value, ok := _c.mutation.AutoPauseOnExpired(); ok {
|
||||||
|
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
|
||||||
|
_node.AutoPauseOnExpired = value
|
||||||
|
}
|
||||||
if value, ok := _c.mutation.Schedulable(); ok {
|
if value, ok := _c.mutation.Schedulable(); ok {
|
||||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||||
_node.Schedulable = value
|
_node.Schedulable = value
|
||||||
@@ -898,6 +941,36 @@ func (u *AccountUpsert) ClearLastUsedAt() *AccountUpsert {
|
|||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (u *AccountUpsert) SetExpiresAt(v time.Time) *AccountUpsert {
|
||||||
|
u.Set(account.FieldExpiresAt, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsert) UpdateExpiresAt() *AccountUpsert {
|
||||||
|
u.SetExcluded(account.FieldExpiresAt)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (u *AccountUpsert) ClearExpiresAt() *AccountUpsert {
|
||||||
|
u.SetNull(account.FieldExpiresAt)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (u *AccountUpsert) SetAutoPauseOnExpired(v bool) *AccountUpsert {
|
||||||
|
u.Set(account.FieldAutoPauseOnExpired, v)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsert) UpdateAutoPauseOnExpired() *AccountUpsert {
|
||||||
|
u.SetExcluded(account.FieldAutoPauseOnExpired)
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (u *AccountUpsert) SetSchedulable(v bool) *AccountUpsert {
|
func (u *AccountUpsert) SetSchedulable(v bool) *AccountUpsert {
|
||||||
u.Set(account.FieldSchedulable, v)
|
u.Set(account.FieldSchedulable, v)
|
||||||
@@ -1308,6 +1381,41 @@ func (u *AccountUpsertOne) ClearLastUsedAt() *AccountUpsertOne {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (u *AccountUpsertOne) SetExpiresAt(v time.Time) *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetExpiresAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertOne) UpdateExpiresAt() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (u *AccountUpsertOne) ClearExpiresAt() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.ClearExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (u *AccountUpsertOne) SetAutoPauseOnExpired(v bool) *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetAutoPauseOnExpired(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertOne) UpdateAutoPauseOnExpired() *AccountUpsertOne {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateAutoPauseOnExpired()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (u *AccountUpsertOne) SetSchedulable(v bool) *AccountUpsertOne {
|
func (u *AccountUpsertOne) SetSchedulable(v bool) *AccountUpsertOne {
|
||||||
return u.Update(func(s *AccountUpsert) {
|
return u.Update(func(s *AccountUpsert) {
|
||||||
@@ -1904,6 +2012,41 @@ func (u *AccountUpsertBulk) ClearLastUsedAt() *AccountUpsertBulk {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (u *AccountUpsertBulk) SetExpiresAt(v time.Time) *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetExpiresAt(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateExpiresAt sets the "expires_at" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertBulk) UpdateExpiresAt() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (u *AccountUpsertBulk) ClearExpiresAt() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.ClearExpiresAt()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (u *AccountUpsertBulk) SetAutoPauseOnExpired(v bool) *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.SetAutoPauseOnExpired(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateAutoPauseOnExpired sets the "auto_pause_on_expired" field to the value that was provided on create.
|
||||||
|
func (u *AccountUpsertBulk) UpdateAutoPauseOnExpired() *AccountUpsertBulk {
|
||||||
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
s.UpdateAutoPauseOnExpired()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (u *AccountUpsertBulk) SetSchedulable(v bool) *AccountUpsertBulk {
|
func (u *AccountUpsertBulk) SetSchedulable(v bool) *AccountUpsertBulk {
|
||||||
return u.Update(func(s *AccountUpsert) {
|
return u.Update(func(s *AccountUpsert) {
|
||||||
|
|||||||
@@ -247,6 +247,40 @@ func (_u *AccountUpdate) ClearLastUsedAt() *AccountUpdate {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (_u *AccountUpdate) SetExpiresAt(v time.Time) *AccountUpdate {
|
||||||
|
_u.mutation.SetExpiresAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdate) SetNillableExpiresAt(v *time.Time) *AccountUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetExpiresAt(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (_u *AccountUpdate) ClearExpiresAt() *AccountUpdate {
|
||||||
|
_u.mutation.ClearExpiresAt()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (_u *AccountUpdate) SetAutoPauseOnExpired(v bool) *AccountUpdate {
|
||||||
|
_u.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdate) SetNillableAutoPauseOnExpired(v *bool) *AccountUpdate {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetAutoPauseOnExpired(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (_u *AccountUpdate) SetSchedulable(v bool) *AccountUpdate {
|
func (_u *AccountUpdate) SetSchedulable(v bool) *AccountUpdate {
|
||||||
_u.mutation.SetSchedulable(v)
|
_u.mutation.SetSchedulable(v)
|
||||||
@@ -610,6 +644,15 @@ func (_u *AccountUpdate) sqlSave(ctx context.Context) (_node int, err error) {
|
|||||||
if _u.mutation.LastUsedAtCleared() {
|
if _u.mutation.LastUsedAtCleared() {
|
||||||
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.ExpiresAt(); ok {
|
||||||
|
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.ExpiresAtCleared() {
|
||||||
|
_spec.ClearField(account.FieldExpiresAt, field.TypeTime)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AutoPauseOnExpired(); ok {
|
||||||
|
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Schedulable(); ok {
|
if value, ok := _u.mutation.Schedulable(); ok {
|
||||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||||
}
|
}
|
||||||
@@ -1016,6 +1059,40 @@ func (_u *AccountUpdateOne) ClearLastUsedAt() *AccountUpdateOne {
|
|||||||
return _u
|
return _u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (_u *AccountUpdateOne) SetExpiresAt(v time.Time) *AccountUpdateOne {
|
||||||
|
_u.mutation.SetExpiresAt(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableExpiresAt sets the "expires_at" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdateOne) SetNillableExpiresAt(v *time.Time) *AccountUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetExpiresAt(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearExpiresAt clears the value of the "expires_at" field.
|
||||||
|
func (_u *AccountUpdateOne) ClearExpiresAt() *AccountUpdateOne {
|
||||||
|
_u.mutation.ClearExpiresAt()
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (_u *AccountUpdateOne) SetAutoPauseOnExpired(v bool) *AccountUpdateOne {
|
||||||
|
_u.mutation.SetAutoPauseOnExpired(v)
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNillableAutoPauseOnExpired sets the "auto_pause_on_expired" field if the given value is not nil.
|
||||||
|
func (_u *AccountUpdateOne) SetNillableAutoPauseOnExpired(v *bool) *AccountUpdateOne {
|
||||||
|
if v != nil {
|
||||||
|
_u.SetAutoPauseOnExpired(*v)
|
||||||
|
}
|
||||||
|
return _u
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (_u *AccountUpdateOne) SetSchedulable(v bool) *AccountUpdateOne {
|
func (_u *AccountUpdateOne) SetSchedulable(v bool) *AccountUpdateOne {
|
||||||
_u.mutation.SetSchedulable(v)
|
_u.mutation.SetSchedulable(v)
|
||||||
@@ -1409,6 +1486,15 @@ func (_u *AccountUpdateOne) sqlSave(ctx context.Context) (_node *Account, err er
|
|||||||
if _u.mutation.LastUsedAtCleared() {
|
if _u.mutation.LastUsedAtCleared() {
|
||||||
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
_spec.ClearField(account.FieldLastUsedAt, field.TypeTime)
|
||||||
}
|
}
|
||||||
|
if value, ok := _u.mutation.ExpiresAt(); ok {
|
||||||
|
_spec.SetField(account.FieldExpiresAt, field.TypeTime, value)
|
||||||
|
}
|
||||||
|
if _u.mutation.ExpiresAtCleared() {
|
||||||
|
_spec.ClearField(account.FieldExpiresAt, field.TypeTime)
|
||||||
|
}
|
||||||
|
if value, ok := _u.mutation.AutoPauseOnExpired(); ok {
|
||||||
|
_spec.SetField(account.FieldAutoPauseOnExpired, field.TypeBool, value)
|
||||||
|
}
|
||||||
if value, ok := _u.mutation.Schedulable(); ok {
|
if value, ok := _u.mutation.Schedulable(); ok {
|
||||||
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
_spec.SetField(account.FieldSchedulable, field.TypeBool, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ var (
|
|||||||
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
{Name: "status", Type: field.TypeString, Size: 20, Default: "active"},
|
||||||
{Name: "error_message", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
|
{Name: "error_message", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}},
|
||||||
{Name: "last_used_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "last_used_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
|
{Name: "expires_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
|
{Name: "auto_pause_on_expired", Type: field.TypeBool, Default: true},
|
||||||
{Name: "schedulable", Type: field.TypeBool, Default: true},
|
{Name: "schedulable", Type: field.TypeBool, Default: true},
|
||||||
{Name: "rate_limited_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "rate_limited_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
{Name: "rate_limit_reset_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{Name: "rate_limit_reset_at", Type: field.TypeTime, Nullable: true, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
@@ -97,7 +99,7 @@ var (
|
|||||||
ForeignKeys: []*schema.ForeignKey{
|
ForeignKeys: []*schema.ForeignKey{
|
||||||
{
|
{
|
||||||
Symbol: "accounts_proxies_proxy",
|
Symbol: "accounts_proxies_proxy",
|
||||||
Columns: []*schema.Column{AccountsColumns[22]},
|
Columns: []*schema.Column{AccountsColumns[24]},
|
||||||
RefColumns: []*schema.Column{ProxiesColumns[0]},
|
RefColumns: []*schema.Column{ProxiesColumns[0]},
|
||||||
OnDelete: schema.SetNull,
|
OnDelete: schema.SetNull,
|
||||||
},
|
},
|
||||||
@@ -121,7 +123,7 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "account_proxy_id",
|
Name: "account_proxy_id",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[22]},
|
Columns: []*schema.Column{AccountsColumns[24]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_priority",
|
Name: "account_priority",
|
||||||
@@ -136,22 +138,22 @@ var (
|
|||||||
{
|
{
|
||||||
Name: "account_schedulable",
|
Name: "account_schedulable",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[15]},
|
Columns: []*schema.Column{AccountsColumns[17]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_rate_limited_at",
|
Name: "account_rate_limited_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[16]},
|
Columns: []*schema.Column{AccountsColumns[18]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_rate_limit_reset_at",
|
Name: "account_rate_limit_reset_at",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[17]},
|
Columns: []*schema.Column{AccountsColumns[19]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_overload_until",
|
Name: "account_overload_until",
|
||||||
Unique: false,
|
Unique: false,
|
||||||
Columns: []*schema.Column{AccountsColumns[18]},
|
Columns: []*schema.Column{AccountsColumns[20]},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "account_deleted_at",
|
Name: "account_deleted_at",
|
||||||
|
|||||||
@@ -1006,6 +1006,8 @@ type AccountMutation struct {
|
|||||||
status *string
|
status *string
|
||||||
error_message *string
|
error_message *string
|
||||||
last_used_at *time.Time
|
last_used_at *time.Time
|
||||||
|
expires_at *time.Time
|
||||||
|
auto_pause_on_expired *bool
|
||||||
schedulable *bool
|
schedulable *bool
|
||||||
rate_limited_at *time.Time
|
rate_limited_at *time.Time
|
||||||
rate_limit_reset_at *time.Time
|
rate_limit_reset_at *time.Time
|
||||||
@@ -1770,6 +1772,91 @@ func (m *AccountMutation) ResetLastUsedAt() {
|
|||||||
delete(m.clearedFields, account.FieldLastUsedAt)
|
delete(m.clearedFields, account.FieldLastUsedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetExpiresAt sets the "expires_at" field.
|
||||||
|
func (m *AccountMutation) SetExpiresAt(t time.Time) {
|
||||||
|
m.expires_at = &t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAt returns the value of the "expires_at" field in the mutation.
|
||||||
|
func (m *AccountMutation) 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 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) 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 *AccountMutation) ClearExpiresAt() {
|
||||||
|
m.expires_at = nil
|
||||||
|
m.clearedFields[account.FieldExpiresAt] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpiresAtCleared returns if the "expires_at" field was cleared in this mutation.
|
||||||
|
func (m *AccountMutation) ExpiresAtCleared() bool {
|
||||||
|
_, ok := m.clearedFields[account.FieldExpiresAt]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetExpiresAt resets all changes to the "expires_at" field.
|
||||||
|
func (m *AccountMutation) ResetExpiresAt() {
|
||||||
|
m.expires_at = nil
|
||||||
|
delete(m.clearedFields, account.FieldExpiresAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAutoPauseOnExpired sets the "auto_pause_on_expired" field.
|
||||||
|
func (m *AccountMutation) SetAutoPauseOnExpired(b bool) {
|
||||||
|
m.auto_pause_on_expired = &b
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoPauseOnExpired returns the value of the "auto_pause_on_expired" field in the mutation.
|
||||||
|
func (m *AccountMutation) AutoPauseOnExpired() (r bool, exists bool) {
|
||||||
|
v := m.auto_pause_on_expired
|
||||||
|
if v == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return *v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// OldAutoPauseOnExpired returns the old "auto_pause_on_expired" 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) OldAutoPauseOnExpired(ctx context.Context) (v bool, err error) {
|
||||||
|
if !m.op.Is(OpUpdateOne) {
|
||||||
|
return v, errors.New("OldAutoPauseOnExpired is only allowed on UpdateOne operations")
|
||||||
|
}
|
||||||
|
if m.id == nil || m.oldValue == nil {
|
||||||
|
return v, errors.New("OldAutoPauseOnExpired requires an ID field in the mutation")
|
||||||
|
}
|
||||||
|
oldValue, err := m.oldValue(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return v, fmt.Errorf("querying old value for OldAutoPauseOnExpired: %w", err)
|
||||||
|
}
|
||||||
|
return oldValue.AutoPauseOnExpired, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetAutoPauseOnExpired resets all changes to the "auto_pause_on_expired" field.
|
||||||
|
func (m *AccountMutation) ResetAutoPauseOnExpired() {
|
||||||
|
m.auto_pause_on_expired = nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetSchedulable sets the "schedulable" field.
|
// SetSchedulable sets the "schedulable" field.
|
||||||
func (m *AccountMutation) SetSchedulable(b bool) {
|
func (m *AccountMutation) SetSchedulable(b bool) {
|
||||||
m.schedulable = &b
|
m.schedulable = &b
|
||||||
@@ -2269,7 +2356,7 @@ func (m *AccountMutation) Type() string {
|
|||||||
// order to get all numeric fields that were incremented/decremented, call
|
// order to get all numeric fields that were incremented/decremented, call
|
||||||
// AddedFields().
|
// AddedFields().
|
||||||
func (m *AccountMutation) Fields() []string {
|
func (m *AccountMutation) Fields() []string {
|
||||||
fields := make([]string, 0, 22)
|
fields := make([]string, 0, 24)
|
||||||
if m.created_at != nil {
|
if m.created_at != nil {
|
||||||
fields = append(fields, account.FieldCreatedAt)
|
fields = append(fields, account.FieldCreatedAt)
|
||||||
}
|
}
|
||||||
@@ -2315,6 +2402,12 @@ func (m *AccountMutation) Fields() []string {
|
|||||||
if m.last_used_at != nil {
|
if m.last_used_at != nil {
|
||||||
fields = append(fields, account.FieldLastUsedAt)
|
fields = append(fields, account.FieldLastUsedAt)
|
||||||
}
|
}
|
||||||
|
if m.expires_at != nil {
|
||||||
|
fields = append(fields, account.FieldExpiresAt)
|
||||||
|
}
|
||||||
|
if m.auto_pause_on_expired != nil {
|
||||||
|
fields = append(fields, account.FieldAutoPauseOnExpired)
|
||||||
|
}
|
||||||
if m.schedulable != nil {
|
if m.schedulable != nil {
|
||||||
fields = append(fields, account.FieldSchedulable)
|
fields = append(fields, account.FieldSchedulable)
|
||||||
}
|
}
|
||||||
@@ -2374,6 +2467,10 @@ func (m *AccountMutation) Field(name string) (ent.Value, bool) {
|
|||||||
return m.ErrorMessage()
|
return m.ErrorMessage()
|
||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
return m.LastUsedAt()
|
return m.LastUsedAt()
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
return m.ExpiresAt()
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
return m.AutoPauseOnExpired()
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
return m.Schedulable()
|
return m.Schedulable()
|
||||||
case account.FieldRateLimitedAt:
|
case account.FieldRateLimitedAt:
|
||||||
@@ -2427,6 +2524,10 @@ func (m *AccountMutation) OldField(ctx context.Context, name string) (ent.Value,
|
|||||||
return m.OldErrorMessage(ctx)
|
return m.OldErrorMessage(ctx)
|
||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
return m.OldLastUsedAt(ctx)
|
return m.OldLastUsedAt(ctx)
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
return m.OldExpiresAt(ctx)
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
return m.OldAutoPauseOnExpired(ctx)
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
return m.OldSchedulable(ctx)
|
return m.OldSchedulable(ctx)
|
||||||
case account.FieldRateLimitedAt:
|
case account.FieldRateLimitedAt:
|
||||||
@@ -2555,6 +2656,20 @@ func (m *AccountMutation) SetField(name string, value ent.Value) error {
|
|||||||
}
|
}
|
||||||
m.SetLastUsedAt(v)
|
m.SetLastUsedAt(v)
|
||||||
return nil
|
return nil
|
||||||
|
case account.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 account.FieldAutoPauseOnExpired:
|
||||||
|
v, ok := value.(bool)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unexpected type %T for field %s", value, name)
|
||||||
|
}
|
||||||
|
m.SetAutoPauseOnExpired(v)
|
||||||
|
return nil
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
v, ok := value.(bool)
|
v, ok := value.(bool)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -2676,6 +2791,9 @@ func (m *AccountMutation) ClearedFields() []string {
|
|||||||
if m.FieldCleared(account.FieldLastUsedAt) {
|
if m.FieldCleared(account.FieldLastUsedAt) {
|
||||||
fields = append(fields, account.FieldLastUsedAt)
|
fields = append(fields, account.FieldLastUsedAt)
|
||||||
}
|
}
|
||||||
|
if m.FieldCleared(account.FieldExpiresAt) {
|
||||||
|
fields = append(fields, account.FieldExpiresAt)
|
||||||
|
}
|
||||||
if m.FieldCleared(account.FieldRateLimitedAt) {
|
if m.FieldCleared(account.FieldRateLimitedAt) {
|
||||||
fields = append(fields, account.FieldRateLimitedAt)
|
fields = append(fields, account.FieldRateLimitedAt)
|
||||||
}
|
}
|
||||||
@@ -2723,6 +2841,9 @@ func (m *AccountMutation) ClearField(name string) error {
|
|||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
m.ClearLastUsedAt()
|
m.ClearLastUsedAt()
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
m.ClearExpiresAt()
|
||||||
|
return nil
|
||||||
case account.FieldRateLimitedAt:
|
case account.FieldRateLimitedAt:
|
||||||
m.ClearRateLimitedAt()
|
m.ClearRateLimitedAt()
|
||||||
return nil
|
return nil
|
||||||
@@ -2794,6 +2915,12 @@ func (m *AccountMutation) ResetField(name string) error {
|
|||||||
case account.FieldLastUsedAt:
|
case account.FieldLastUsedAt:
|
||||||
m.ResetLastUsedAt()
|
m.ResetLastUsedAt()
|
||||||
return nil
|
return nil
|
||||||
|
case account.FieldExpiresAt:
|
||||||
|
m.ResetExpiresAt()
|
||||||
|
return nil
|
||||||
|
case account.FieldAutoPauseOnExpired:
|
||||||
|
m.ResetAutoPauseOnExpired()
|
||||||
|
return nil
|
||||||
case account.FieldSchedulable:
|
case account.FieldSchedulable:
|
||||||
m.ResetSchedulable()
|
m.ResetSchedulable()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -181,12 +181,16 @@ func init() {
|
|||||||
account.DefaultStatus = accountDescStatus.Default.(string)
|
account.DefaultStatus = accountDescStatus.Default.(string)
|
||||||
// account.StatusValidator is a validator for the "status" field. It is called by the builders before save.
|
// 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)
|
account.StatusValidator = accountDescStatus.Validators[0].(func(string) error)
|
||||||
|
// accountDescAutoPauseOnExpired is the schema descriptor for auto_pause_on_expired field.
|
||||||
|
accountDescAutoPauseOnExpired := accountFields[13].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 is the schema descriptor for schedulable field.
|
||||||
accountDescSchedulable := accountFields[12].Descriptor()
|
accountDescSchedulable := accountFields[14].Descriptor()
|
||||||
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
|
// account.DefaultSchedulable holds the default value on creation for the schedulable field.
|
||||||
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
|
account.DefaultSchedulable = accountDescSchedulable.Default.(bool)
|
||||||
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
|
// accountDescSessionWindowStatus is the schema descriptor for session_window_status field.
|
||||||
accountDescSessionWindowStatus := accountFields[18].Descriptor()
|
accountDescSessionWindowStatus := accountFields[20].Descriptor()
|
||||||
// account.SessionWindowStatusValidator is a validator for the "session_window_status" field. It is called by the builders before save.
|
// 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)
|
account.SessionWindowStatusValidator = accountDescSessionWindowStatus.Validators[0].(func(string) error)
|
||||||
accountgroupFields := schema.AccountGroup{}.Fields()
|
accountgroupFields := schema.AccountGroup{}.Fields()
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ func (Account) Fields() []ent.Field {
|
|||||||
Optional().
|
Optional().
|
||||||
Nillable().
|
Nillable().
|
||||||
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||||
|
// expires_at: 账户过期时间(可为空)
|
||||||
|
field.Time("expires_at").
|
||||||
|
Optional().
|
||||||
|
Nillable().
|
||||||
|
Comment("Account expiration time (NULL means no expiration).").
|
||||||
|
SchemaType(map[string]string{dialect.Postgres: "timestamptz"}),
|
||||||
|
// auto_pause_on_expired: 过期后自动暂停调度
|
||||||
|
field.Bool("auto_pause_on_expired").
|
||||||
|
Default(true).
|
||||||
|
Comment("Auto pause scheduling when account expires."),
|
||||||
|
|
||||||
// ========== 调度和速率限制相关字段 ==========
|
// ========== 调度和速率限制相关字段 ==========
|
||||||
// 这些字段在 migrations/005_schema_parity.sql 中添加
|
// 这些字段在 migrations/005_schema_parity.sql 中添加
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ type CreateAccountRequest struct {
|
|||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
GroupIDs []int64 `json:"group_ids"`
|
GroupIDs []int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +103,8 @@ type UpdateAccountRequest struct {
|
|||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
Status string `json:"status" binding:"omitempty,oneof=active inactive"`
|
||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
ConfirmMixedChannelRisk *bool `json:"confirm_mixed_channel_risk"` // 用户确认混合渠道风险
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,6 +208,8 @@ func (h *AccountHandler) Create(c *gin.Context) {
|
|||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
|
ExpiresAt: req.ExpiresAt,
|
||||||
|
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||||
SkipMixedChannelCheck: skipCheck,
|
SkipMixedChannelCheck: skipCheck,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -261,6 +267,8 @@ func (h *AccountHandler) Update(c *gin.Context) {
|
|||||||
Priority: req.Priority, // 指针类型,nil 表示未提供
|
Priority: req.Priority, // 指针类型,nil 表示未提供
|
||||||
Status: req.Status,
|
Status: req.Status,
|
||||||
GroupIDs: req.GroupIDs,
|
GroupIDs: req.GroupIDs,
|
||||||
|
ExpiresAt: req.ExpiresAt,
|
||||||
|
AutoPauseOnExpired: req.AutoPauseOnExpired,
|
||||||
SkipMixedChannelCheck: skipCheck,
|
SkipMixedChannelCheck: skipCheck,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
// Package dto provides data transfer objects for HTTP handlers.
|
// Package dto provides data transfer objects for HTTP handlers.
|
||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "github.com/Wei-Shaw/sub2api/internal/service"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
||||||
|
)
|
||||||
|
|
||||||
func UserFromServiceShallow(u *service.User) *User {
|
func UserFromServiceShallow(u *service.User) *User {
|
||||||
if u == nil {
|
if u == nil {
|
||||||
@@ -120,6 +124,8 @@ func AccountFromServiceShallow(a *service.Account) *Account {
|
|||||||
Status: a.Status,
|
Status: a.Status,
|
||||||
ErrorMessage: a.ErrorMessage,
|
ErrorMessage: a.ErrorMessage,
|
||||||
LastUsedAt: a.LastUsedAt,
|
LastUsedAt: a.LastUsedAt,
|
||||||
|
ExpiresAt: timeToUnixSeconds(a.ExpiresAt),
|
||||||
|
AutoPauseOnExpired: a.AutoPauseOnExpired,
|
||||||
CreatedAt: a.CreatedAt,
|
CreatedAt: a.CreatedAt,
|
||||||
UpdatedAt: a.UpdatedAt,
|
UpdatedAt: a.UpdatedAt,
|
||||||
Schedulable: a.Schedulable,
|
Schedulable: a.Schedulable,
|
||||||
@@ -157,6 +163,14 @@ func AccountFromService(a *service.Account) *Account {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func timeToUnixSeconds(value *time.Time) *int64 {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ts := value.Unix()
|
||||||
|
return &ts
|
||||||
|
}
|
||||||
|
|
||||||
func AccountGroupFromService(ag *service.AccountGroup) *AccountGroup {
|
func AccountGroupFromService(ag *service.AccountGroup) *AccountGroup {
|
||||||
if ag == nil {
|
if ag == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -60,21 +60,23 @@ type Group struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Notes *string `json:"notes"`
|
Notes *string `json:"notes"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Credentials map[string]any `json:"credentials"`
|
Credentials map[string]any `json:"credentials"`
|
||||||
Extra map[string]any `json:"extra"`
|
Extra map[string]any `json:"extra"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
ErrorMessage string `json:"error_message"`
|
ErrorMessage string `json:"error_message"`
|
||||||
LastUsedAt *time.Time `json:"last_used_at"`
|
LastUsedAt *time.Time `json:"last_used_at"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
ExpiresAt *int64 `json:"expires_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
AutoPauseOnExpired bool `json:"auto_pause_on_expired"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
Schedulable bool `json:"schedulable"`
|
Schedulable bool `json:"schedulable"`
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
|
|||||||
SetPriority(account.Priority).
|
SetPriority(account.Priority).
|
||||||
SetStatus(account.Status).
|
SetStatus(account.Status).
|
||||||
SetErrorMessage(account.ErrorMessage).
|
SetErrorMessage(account.ErrorMessage).
|
||||||
SetSchedulable(account.Schedulable)
|
SetSchedulable(account.Schedulable).
|
||||||
|
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
|
||||||
|
|
||||||
if account.ProxyID != nil {
|
if account.ProxyID != nil {
|
||||||
builder.SetProxyID(*account.ProxyID)
|
builder.SetProxyID(*account.ProxyID)
|
||||||
@@ -84,6 +85,9 @@ func (r *accountRepository) Create(ctx context.Context, account *service.Account
|
|||||||
if account.LastUsedAt != nil {
|
if account.LastUsedAt != nil {
|
||||||
builder.SetLastUsedAt(*account.LastUsedAt)
|
builder.SetLastUsedAt(*account.LastUsedAt)
|
||||||
}
|
}
|
||||||
|
if account.ExpiresAt != nil {
|
||||||
|
builder.SetExpiresAt(*account.ExpiresAt)
|
||||||
|
}
|
||||||
if account.RateLimitedAt != nil {
|
if account.RateLimitedAt != nil {
|
||||||
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
||||||
}
|
}
|
||||||
@@ -280,7 +284,8 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
|
|||||||
SetPriority(account.Priority).
|
SetPriority(account.Priority).
|
||||||
SetStatus(account.Status).
|
SetStatus(account.Status).
|
||||||
SetErrorMessage(account.ErrorMessage).
|
SetErrorMessage(account.ErrorMessage).
|
||||||
SetSchedulable(account.Schedulable)
|
SetSchedulable(account.Schedulable).
|
||||||
|
SetAutoPauseOnExpired(account.AutoPauseOnExpired)
|
||||||
|
|
||||||
if account.ProxyID != nil {
|
if account.ProxyID != nil {
|
||||||
builder.SetProxyID(*account.ProxyID)
|
builder.SetProxyID(*account.ProxyID)
|
||||||
@@ -292,6 +297,11 @@ func (r *accountRepository) Update(ctx context.Context, account *service.Account
|
|||||||
} else {
|
} else {
|
||||||
builder.ClearLastUsedAt()
|
builder.ClearLastUsedAt()
|
||||||
}
|
}
|
||||||
|
if account.ExpiresAt != nil {
|
||||||
|
builder.SetExpiresAt(*account.ExpiresAt)
|
||||||
|
} else {
|
||||||
|
builder.ClearExpiresAt()
|
||||||
|
}
|
||||||
if account.RateLimitedAt != nil {
|
if account.RateLimitedAt != nil {
|
||||||
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
builder.SetRateLimitedAt(*account.RateLimitedAt)
|
||||||
} else {
|
} else {
|
||||||
@@ -570,6 +580,7 @@ func (r *accountRepository) ListSchedulable(ctx context.Context) ([]service.Acco
|
|||||||
dbaccount.StatusEQ(service.StatusActive),
|
dbaccount.StatusEQ(service.StatusActive),
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
).
|
).
|
||||||
@@ -596,6 +607,7 @@ func (r *accountRepository) ListSchedulableByPlatform(ctx context.Context, platf
|
|||||||
dbaccount.StatusEQ(service.StatusActive),
|
dbaccount.StatusEQ(service.StatusActive),
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
).
|
).
|
||||||
@@ -629,6 +641,7 @@ func (r *accountRepository) ListSchedulableByPlatforms(ctx context.Context, plat
|
|||||||
dbaccount.StatusEQ(service.StatusActive),
|
dbaccount.StatusEQ(service.StatusActive),
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
).
|
).
|
||||||
@@ -727,6 +740,27 @@ func (r *accountRepository) SetSchedulable(ctx context.Context, id int64, schedu
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *accountRepository) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
result, err := r.sql.ExecContext(ctx, `
|
||||||
|
UPDATE accounts
|
||||||
|
SET schedulable = FALSE,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND schedulable = TRUE
|
||||||
|
AND auto_pause_on_expired = TRUE
|
||||||
|
AND expires_at IS NOT NULL
|
||||||
|
AND expires_at <= $1
|
||||||
|
`, now)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
rows, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return rows, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
|
func (r *accountRepository) UpdateExtra(ctx context.Context, id int64, updates map[string]any) error {
|
||||||
if len(updates) == 0 {
|
if len(updates) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -861,6 +895,7 @@ func (r *accountRepository) queryAccountsByGroup(ctx context.Context, groupID in
|
|||||||
preds = append(preds,
|
preds = append(preds,
|
||||||
dbaccount.SchedulableEQ(true),
|
dbaccount.SchedulableEQ(true),
|
||||||
tempUnschedulablePredicate(),
|
tempUnschedulablePredicate(),
|
||||||
|
notExpiredPredicate(now),
|
||||||
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
dbaccount.Or(dbaccount.OverloadUntilIsNil(), dbaccount.OverloadUntilLTE(now)),
|
||||||
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
dbaccount.Or(dbaccount.RateLimitResetAtIsNil(), dbaccount.RateLimitResetAtLTE(now)),
|
||||||
)
|
)
|
||||||
@@ -971,6 +1006,14 @@ func tempUnschedulablePredicate() dbpredicate.Account {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notExpiredPredicate(now time.Time) dbpredicate.Account {
|
||||||
|
return dbaccount.Or(
|
||||||
|
dbaccount.ExpiresAtIsNil(),
|
||||||
|
dbaccount.ExpiresAtGT(now),
|
||||||
|
dbaccount.AutoPauseOnExpiredEQ(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *accountRepository) loadTempUnschedStates(ctx context.Context, accountIDs []int64) (map[int64]tempUnschedSnapshot, error) {
|
func (r *accountRepository) loadTempUnschedStates(ctx context.Context, accountIDs []int64) (map[int64]tempUnschedSnapshot, error) {
|
||||||
out := make(map[int64]tempUnschedSnapshot)
|
out := make(map[int64]tempUnschedSnapshot)
|
||||||
if len(accountIDs) == 0 {
|
if len(accountIDs) == 0 {
|
||||||
@@ -1086,6 +1129,8 @@ func accountEntityToService(m *dbent.Account) *service.Account {
|
|||||||
Status: m.Status,
|
Status: m.Status,
|
||||||
ErrorMessage: derefString(m.ErrorMessage),
|
ErrorMessage: derefString(m.ErrorMessage),
|
||||||
LastUsedAt: m.LastUsedAt,
|
LastUsedAt: m.LastUsedAt,
|
||||||
|
ExpiresAt: m.ExpiresAt,
|
||||||
|
AutoPauseOnExpired: m.AutoPauseOnExpired,
|
||||||
CreatedAt: m.CreatedAt,
|
CreatedAt: m.CreatedAt,
|
||||||
UpdatedAt: m.UpdatedAt,
|
UpdatedAt: m.UpdatedAt,
|
||||||
Schedulable: m.Schedulable,
|
Schedulable: m.Schedulable,
|
||||||
|
|||||||
@@ -9,21 +9,23 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
ID int64
|
ID int64
|
||||||
Name string
|
Name string
|
||||||
Notes *string
|
Notes *string
|
||||||
Platform string
|
Platform string
|
||||||
Type string
|
Type string
|
||||||
Credentials map[string]any
|
Credentials map[string]any
|
||||||
Extra map[string]any
|
Extra map[string]any
|
||||||
ProxyID *int64
|
ProxyID *int64
|
||||||
Concurrency int
|
Concurrency int
|
||||||
Priority int
|
Priority int
|
||||||
Status string
|
Status string
|
||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
LastUsedAt *time.Time
|
LastUsedAt *time.Time
|
||||||
CreatedAt time.Time
|
ExpiresAt *time.Time
|
||||||
UpdatedAt time.Time
|
AutoPauseOnExpired bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
|
||||||
Schedulable bool
|
Schedulable bool
|
||||||
|
|
||||||
@@ -60,6 +62,9 @@ func (a *Account) IsSchedulable() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
if a.AutoPauseOnExpired && a.ExpiresAt != nil && !now.Before(*a.ExpiresAt) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) {
|
if a.OverloadUntil != nil && now.Before(*a.OverloadUntil) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
71
backend/internal/service/account_expiry_service.go
Normal file
71
backend/internal/service/account_expiry_service.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AccountExpiryService periodically pauses expired accounts when auto-pause is enabled.
|
||||||
|
type AccountExpiryService struct {
|
||||||
|
accountRepo AccountRepository
|
||||||
|
interval time.Duration
|
||||||
|
stopCh chan struct{}
|
||||||
|
stopOnce sync.Once
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAccountExpiryService(accountRepo AccountRepository, interval time.Duration) *AccountExpiryService {
|
||||||
|
return &AccountExpiryService{
|
||||||
|
accountRepo: accountRepo,
|
||||||
|
interval: interval,
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccountExpiryService) Start() {
|
||||||
|
if s == nil || s.accountRepo == 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 *AccountExpiryService) Stop() {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.stopOnce.Do(func() {
|
||||||
|
close(s.stopCh)
|
||||||
|
})
|
||||||
|
s.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AccountExpiryService) runOnce() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
updated, err := s.accountRepo.AutoPauseExpiredAccounts(ctx, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[AccountExpiry] Auto pause expired accounts failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if updated > 0 {
|
||||||
|
log.Printf("[AccountExpiry] Auto paused %d expired accounts", updated)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ type AccountRepository interface {
|
|||||||
BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error
|
BatchUpdateLastUsed(ctx context.Context, updates map[int64]time.Time) error
|
||||||
SetError(ctx context.Context, id int64, errorMsg string) error
|
SetError(ctx context.Context, id int64, errorMsg string) error
|
||||||
SetSchedulable(ctx context.Context, id int64, schedulable bool) error
|
SetSchedulable(ctx context.Context, id int64, schedulable bool) error
|
||||||
|
AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error)
|
||||||
BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error
|
BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error
|
||||||
|
|
||||||
ListSchedulable(ctx context.Context) ([]Account, error)
|
ListSchedulable(ctx context.Context) ([]Account, error)
|
||||||
@@ -71,29 +72,33 @@ type AccountBulkUpdate struct {
|
|||||||
|
|
||||||
// CreateAccountRequest 创建账号请求
|
// CreateAccountRequest 创建账号请求
|
||||||
type CreateAccountRequest struct {
|
type CreateAccountRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Notes *string `json:"notes"`
|
Notes *string `json:"notes"`
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Credentials map[string]any `json:"credentials"`
|
Credentials map[string]any `json:"credentials"`
|
||||||
Extra map[string]any `json:"extra"`
|
Extra map[string]any `json:"extra"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Concurrency int `json:"concurrency"`
|
Concurrency int `json:"concurrency"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
GroupIDs []int64 `json:"group_ids"`
|
GroupIDs []int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateAccountRequest 更新账号请求
|
// UpdateAccountRequest 更新账号请求
|
||||||
type UpdateAccountRequest struct {
|
type UpdateAccountRequest struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Notes *string `json:"notes"`
|
Notes *string `json:"notes"`
|
||||||
Credentials *map[string]any `json:"credentials"`
|
Credentials *map[string]any `json:"credentials"`
|
||||||
Extra *map[string]any `json:"extra"`
|
Extra *map[string]any `json:"extra"`
|
||||||
ProxyID *int64 `json:"proxy_id"`
|
ProxyID *int64 `json:"proxy_id"`
|
||||||
Concurrency *int `json:"concurrency"`
|
Concurrency *int `json:"concurrency"`
|
||||||
Priority *int `json:"priority"`
|
Priority *int `json:"priority"`
|
||||||
Status *string `json:"status"`
|
Status *string `json:"status"`
|
||||||
GroupIDs *[]int64 `json:"group_ids"`
|
GroupIDs *[]int64 `json:"group_ids"`
|
||||||
|
ExpiresAt *time.Time `json:"expires_at"`
|
||||||
|
AutoPauseOnExpired *bool `json:"auto_pause_on_expired"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccountService 账号管理服务
|
// AccountService 账号管理服务
|
||||||
@@ -134,6 +139,12 @@ func (s *AccountService) Create(ctx context.Context, req CreateAccountRequest) (
|
|||||||
Concurrency: req.Concurrency,
|
Concurrency: req.Concurrency,
|
||||||
Priority: req.Priority,
|
Priority: req.Priority,
|
||||||
Status: StatusActive,
|
Status: StatusActive,
|
||||||
|
ExpiresAt: req.ExpiresAt,
|
||||||
|
}
|
||||||
|
if req.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *req.AutoPauseOnExpired
|
||||||
|
} else {
|
||||||
|
account.AutoPauseOnExpired = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.accountRepo.Create(ctx, account); err != nil {
|
if err := s.accountRepo.Create(ctx, account); err != nil {
|
||||||
@@ -224,6 +235,12 @@ func (s *AccountService) Update(ctx context.Context, id int64, req UpdateAccount
|
|||||||
if req.Status != nil {
|
if req.Status != nil {
|
||||||
account.Status = *req.Status
|
account.Status = *req.Status
|
||||||
}
|
}
|
||||||
|
if req.ExpiresAt != nil {
|
||||||
|
account.ExpiresAt = req.ExpiresAt
|
||||||
|
}
|
||||||
|
if req.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *req.AutoPauseOnExpired
|
||||||
|
}
|
||||||
|
|
||||||
// 先验证分组是否存在(在任何写操作之前)
|
// 先验证分组是否存在(在任何写操作之前)
|
||||||
if req.GroupIDs != nil {
|
if req.GroupIDs != nil {
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ func (s *accountRepoStub) SetSchedulable(ctx context.Context, id int64, schedula
|
|||||||
panic("unexpected SetSchedulable call")
|
panic("unexpected SetSchedulable call")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *accountRepoStub) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
panic("unexpected AutoPauseExpiredAccounts call")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *accountRepoStub) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
func (s *accountRepoStub) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
||||||
panic("unexpected BindGroups call")
|
panic("unexpected BindGroups call")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,16 +122,18 @@ type UpdateGroupInput struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateAccountInput struct {
|
type CreateAccountInput struct {
|
||||||
Name string
|
Name string
|
||||||
Notes *string
|
Notes *string
|
||||||
Platform string
|
Platform string
|
||||||
Type string
|
Type string
|
||||||
Credentials map[string]any
|
Credentials map[string]any
|
||||||
Extra map[string]any
|
Extra map[string]any
|
||||||
ProxyID *int64
|
ProxyID *int64
|
||||||
Concurrency int
|
Concurrency int
|
||||||
Priority int
|
Priority int
|
||||||
GroupIDs []int64
|
GroupIDs []int64
|
||||||
|
ExpiresAt *int64
|
||||||
|
AutoPauseOnExpired *bool
|
||||||
// SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
|
// SkipMixedChannelCheck skips the mixed channel risk check when binding groups.
|
||||||
// This should only be set when the caller has explicitly confirmed the risk.
|
// This should only be set when the caller has explicitly confirmed the risk.
|
||||||
SkipMixedChannelCheck bool
|
SkipMixedChannelCheck bool
|
||||||
@@ -148,6 +150,8 @@ type UpdateAccountInput struct {
|
|||||||
Priority *int // 使用指针区分"未提供"和"设置为0"
|
Priority *int // 使用指针区分"未提供"和"设置为0"
|
||||||
Status string
|
Status string
|
||||||
GroupIDs *[]int64
|
GroupIDs *[]int64
|
||||||
|
ExpiresAt *int64
|
||||||
|
AutoPauseOnExpired *bool
|
||||||
SkipMixedChannelCheck bool // 跳过混合渠道检查(用户已确认风险)
|
SkipMixedChannelCheck bool // 跳过混合渠道检查(用户已确认风险)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -700,6 +704,15 @@ func (s *adminServiceImpl) CreateAccount(ctx context.Context, input *CreateAccou
|
|||||||
Status: StatusActive,
|
Status: StatusActive,
|
||||||
Schedulable: true,
|
Schedulable: true,
|
||||||
}
|
}
|
||||||
|
if input.ExpiresAt != nil && *input.ExpiresAt > 0 {
|
||||||
|
expiresAt := time.Unix(*input.ExpiresAt, 0)
|
||||||
|
account.ExpiresAt = &expiresAt
|
||||||
|
}
|
||||||
|
if input.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *input.AutoPauseOnExpired
|
||||||
|
} else {
|
||||||
|
account.AutoPauseOnExpired = true
|
||||||
|
}
|
||||||
if err := s.accountRepo.Create(ctx, account); err != nil {
|
if err := s.accountRepo.Create(ctx, account); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -755,6 +768,17 @@ func (s *adminServiceImpl) UpdateAccount(ctx context.Context, id int64, input *U
|
|||||||
if input.Status != "" {
|
if input.Status != "" {
|
||||||
account.Status = input.Status
|
account.Status = input.Status
|
||||||
}
|
}
|
||||||
|
if input.ExpiresAt != nil {
|
||||||
|
if *input.ExpiresAt <= 0 {
|
||||||
|
account.ExpiresAt = nil
|
||||||
|
} else {
|
||||||
|
expiresAt := time.Unix(*input.ExpiresAt, 0)
|
||||||
|
account.ExpiresAt = &expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if input.AutoPauseOnExpired != nil {
|
||||||
|
account.AutoPauseOnExpired = *input.AutoPauseOnExpired
|
||||||
|
}
|
||||||
|
|
||||||
// 先验证分组是否存在(在任何写操作之前)
|
// 先验证分组是否存在(在任何写操作之前)
|
||||||
if input.GroupIDs != nil {
|
if input.GroupIDs != nil {
|
||||||
|
|||||||
@@ -105,6 +105,9 @@ func (m *mockAccountRepoForPlatform) SetError(ctx context.Context, id int64, err
|
|||||||
func (m *mockAccountRepoForPlatform) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
func (m *mockAccountRepoForPlatform) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (m *mockAccountRepoForPlatform) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
func (m *mockAccountRepoForPlatform) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
func (m *mockAccountRepoForPlatform) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,9 @@ func (m *mockAccountRepoForGemini) SetError(ctx context.Context, id int64, error
|
|||||||
func (m *mockAccountRepoForGemini) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
func (m *mockAccountRepoForGemini) SetSchedulable(ctx context.Context, id int64, schedulable bool) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func (m *mockAccountRepoForGemini) AutoPauseExpiredAccounts(ctx context.Context, now time.Time) (int64, error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
func (m *mockAccountRepoForGemini) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
func (m *mockAccountRepoForGemini) BindGroups(ctx context.Context, accountID int64, groupIDs []int64) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,13 @@ func ProvideTokenRefreshService(
|
|||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ProvideAccountExpiryService creates and starts AccountExpiryService.
|
||||||
|
func ProvideAccountExpiryService(accountRepo AccountRepository) *AccountExpiryService {
|
||||||
|
svc := NewAccountExpiryService(accountRepo, time.Minute)
|
||||||
|
svc.Start()
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
// ProvideTimingWheelService creates and starts TimingWheelService
|
// ProvideTimingWheelService creates and starts TimingWheelService
|
||||||
func ProvideTimingWheelService() *TimingWheelService {
|
func ProvideTimingWheelService() *TimingWheelService {
|
||||||
svc := NewTimingWheelService()
|
svc := NewTimingWheelService()
|
||||||
@@ -110,6 +117,7 @@ var ProviderSet = wire.NewSet(
|
|||||||
NewCRSSyncService,
|
NewCRSSyncService,
|
||||||
ProvideUpdateService,
|
ProvideUpdateService,
|
||||||
ProvideTokenRefreshService,
|
ProvideTokenRefreshService,
|
||||||
|
ProvideAccountExpiryService,
|
||||||
ProvideTimingWheelService,
|
ProvideTimingWheelService,
|
||||||
ProvideDeferredService,
|
ProvideDeferredService,
|
||||||
NewAntigravityQuotaFetcher,
|
NewAntigravityQuotaFetcher,
|
||||||
|
|||||||
10
backend/migrations/030_add_account_expires_at.sql
Normal file
10
backend/migrations/030_add_account_expires_at.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- Add expires_at for account expiration configuration
|
||||||
|
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS expires_at timestamptz;
|
||||||
|
-- Document expires_at meaning
|
||||||
|
COMMENT ON COLUMN accounts.expires_at IS 'Account expiration time (NULL means no expiration).';
|
||||||
|
-- Add auto_pause_on_expired for account expiration scheduling control
|
||||||
|
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS auto_pause_on_expired boolean NOT NULL DEFAULT true;
|
||||||
|
-- Document auto_pause_on_expired meaning
|
||||||
|
COMMENT ON COLUMN accounts.auto_pause_on_expired IS 'Auto pause scheduling when account expires.';
|
||||||
|
-- Ensure existing accounts are enabled by default
|
||||||
|
UPDATE accounts SET auto_pause_on_expired = true;
|
||||||
@@ -1012,7 +1012,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Temp Unschedulable Rules -->
|
<!-- Temp Unschedulable Rules -->
|
||||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
||||||
@@ -1213,46 +1213,81 @@
|
|||||||
<p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p>
|
<p class="input-hint">{{ t('admin.accounts.priorityHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
|
<label class="input-label">{{ t('admin.accounts.expiresAt') }}</label>
|
||||||
|
<input v-model="expiresAtInput" type="datetime-local" class="input" />
|
||||||
|
<p class="input-hint">{{ t('admin.accounts.expiresAtHint') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Mixed Scheduling (only for antigravity accounts) -->
|
<div>
|
||||||
<div v-if="form.platform === 'antigravity'" class="flex items-center gap-2">
|
<div class="flex items-center justify-between">
|
||||||
<label class="flex cursor-pointer items-center gap-2">
|
<div>
|
||||||
<input
|
<label class="input-label mb-0">{{
|
||||||
type="checkbox"
|
t('admin.accounts.autoPauseOnExpired')
|
||||||
v-model="mixedScheduling"
|
}}</label>
|
||||||
class="h-4 w-4 rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
/>
|
{{ t('admin.accounts.autoPauseOnExpiredDesc') }}
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
</p>
|
||||||
{{ t('admin.accounts.mixedScheduling') }}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div class="group relative">
|
|
||||||
<span
|
|
||||||
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
|
||||||
>
|
|
||||||
?
|
|
||||||
</span>
|
|
||||||
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
|
||||||
<div
|
|
||||||
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
|
||||||
>
|
|
||||||
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
|
||||||
<div
|
|
||||||
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="autoPauseOnExpired = !autoPauseOnExpired"
|
||||||
|
:class="[
|
||||||
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
|
autoPauseOnExpired ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
|
autoPauseOnExpired ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Group Selection - 仅标准模式显示 -->
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<GroupSelector
|
<!-- Mixed Scheduling (only for antigravity accounts) -->
|
||||||
v-if="!authStore.isSimpleMode"
|
<div v-if="form.platform === 'antigravity'" class="flex items-center gap-2">
|
||||||
v-model="form.group_ids"
|
<label class="flex cursor-pointer items-center gap-2">
|
||||||
:groups="groups"
|
<input
|
||||||
:platform="form.platform"
|
type="checkbox"
|
||||||
:mixed-scheduling="mixedScheduling"
|
v-model="mixedScheduling"
|
||||||
data-tour="account-form-groups"
|
class="h-4 w-4 rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
||||||
/>
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('admin.accounts.mixedScheduling') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="group relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
||||||
|
<div
|
||||||
|
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Group Selection - 仅标准模式显示 -->
|
||||||
|
<GroupSelector
|
||||||
|
v-if="!authStore.isSimpleMode"
|
||||||
|
v-model="form.group_ids"
|
||||||
|
:groups="groups"
|
||||||
|
:platform="form.platform"
|
||||||
|
:mixed-scheduling="mixedScheduling"
|
||||||
|
data-tour="account-form-groups"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -1598,6 +1633,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
import OAuthAuthorizationFlow from './OAuthAuthorizationFlow.vue'
|
||||||
|
|
||||||
// Type for exposed OAuthAuthorizationFlow component
|
// Type for exposed OAuthAuthorizationFlow component
|
||||||
@@ -1713,6 +1749,7 @@ const customErrorCodesEnabled = ref(false)
|
|||||||
const selectedErrorCodes = ref<number[]>([])
|
const selectedErrorCodes = ref<number[]>([])
|
||||||
const customErrorCodeInput = ref<number | null>(null)
|
const customErrorCodeInput = ref<number | null>(null)
|
||||||
const interceptWarmupRequests = ref(false)
|
const interceptWarmupRequests = ref(false)
|
||||||
|
const autoPauseOnExpired = ref(true)
|
||||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||||
const tempUnschedEnabled = ref(false)
|
const tempUnschedEnabled = ref(false)
|
||||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||||
@@ -1795,7 +1832,8 @@ const form = reactive({
|
|||||||
proxy_id: null as number | null,
|
proxy_id: null as number | null,
|
||||||
concurrency: 10,
|
concurrency: 10,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
group_ids: [] as number[]
|
group_ids: [] as number[],
|
||||||
|
expires_at: null as number | null
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper to check if current type needs OAuth flow
|
// Helper to check if current type needs OAuth flow
|
||||||
@@ -1805,6 +1843,13 @@ const isManualInputMethod = computed(() => {
|
|||||||
return oauthFlowRef.value?.inputMethod === 'manual'
|
return oauthFlowRef.value?.inputMethod === 'manual'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const expiresAtInput = computed({
|
||||||
|
get: () => formatDateTimeLocal(form.expires_at),
|
||||||
|
set: (value: string) => {
|
||||||
|
form.expires_at = parseDateTimeLocal(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const canExchangeCode = computed(() => {
|
const canExchangeCode = computed(() => {
|
||||||
const authCode = oauthFlowRef.value?.authCode || ''
|
const authCode = oauthFlowRef.value?.authCode || ''
|
||||||
if (form.platform === 'openai') {
|
if (form.platform === 'openai') {
|
||||||
@@ -2055,6 +2100,7 @@ const resetForm = () => {
|
|||||||
form.concurrency = 10
|
form.concurrency = 10
|
||||||
form.priority = 1
|
form.priority = 1
|
||||||
form.group_ids = []
|
form.group_ids = []
|
||||||
|
form.expires_at = null
|
||||||
accountCategory.value = 'oauth-based'
|
accountCategory.value = 'oauth-based'
|
||||||
addMethod.value = 'oauth'
|
addMethod.value = 'oauth'
|
||||||
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
apiKeyBaseUrl.value = 'https://api.anthropic.com'
|
||||||
@@ -2066,6 +2112,7 @@ const resetForm = () => {
|
|||||||
selectedErrorCodes.value = []
|
selectedErrorCodes.value = []
|
||||||
customErrorCodeInput.value = null
|
customErrorCodeInput.value = null
|
||||||
interceptWarmupRequests.value = false
|
interceptWarmupRequests.value = false
|
||||||
|
autoPauseOnExpired.value = true
|
||||||
tempUnschedEnabled.value = false
|
tempUnschedEnabled.value = false
|
||||||
tempUnschedRules.value = []
|
tempUnschedRules.value = []
|
||||||
geminiOAuthType.value = 'code_assist'
|
geminiOAuthType.value = 'code_assist'
|
||||||
@@ -2133,7 +2180,6 @@ const handleSubmit = async () => {
|
|||||||
if (interceptWarmupRequests.value) {
|
if (interceptWarmupRequests.value) {
|
||||||
credentials.intercept_warmup_requests = true
|
credentials.intercept_warmup_requests = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applyTempUnschedConfig(credentials)) {
|
if (!applyTempUnschedConfig(credentials)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2144,7 +2190,8 @@ const handleSubmit = async () => {
|
|||||||
try {
|
try {
|
||||||
await adminAPI.accounts.create({
|
await adminAPI.accounts.create({
|
||||||
...form,
|
...form,
|
||||||
group_ids: form.group_ids
|
group_ids: form.group_ids,
|
||||||
|
auto_pause_on_expired: autoPauseOnExpired.value
|
||||||
})
|
})
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||||
emit('created')
|
emit('created')
|
||||||
@@ -2182,6 +2229,9 @@ const handleGenerateUrl = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDateTimeLocal = formatDateTimeLocalInput
|
||||||
|
const parseDateTimeLocal = parseDateTimeLocalInput
|
||||||
|
|
||||||
// Create account and handle success/failure
|
// Create account and handle success/failure
|
||||||
const createAccountAndFinish = async (
|
const createAccountAndFinish = async (
|
||||||
platform: AccountPlatform,
|
platform: AccountPlatform,
|
||||||
@@ -2202,7 +2252,9 @@ const createAccountAndFinish = async (
|
|||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
priority: form.priority,
|
priority: form.priority,
|
||||||
group_ids: form.group_ids
|
group_ids: form.group_ids,
|
||||||
|
expires_at: form.expires_at,
|
||||||
|
auto_pause_on_expired: autoPauseOnExpired.value
|
||||||
})
|
})
|
||||||
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
appStore.showSuccess(t('admin.accounts.accountCreated'))
|
||||||
emit('created')
|
emit('created')
|
||||||
@@ -2416,7 +2468,8 @@ const handleCookieAuth = async (sessionKey: string) => {
|
|||||||
extra,
|
extra,
|
||||||
proxy_id: form.proxy_id,
|
proxy_id: form.proxy_id,
|
||||||
concurrency: form.concurrency,
|
concurrency: form.concurrency,
|
||||||
priority: form.priority
|
priority: form.priority,
|
||||||
|
auto_pause_on_expired: autoPauseOnExpired.value
|
||||||
})
|
})
|
||||||
|
|
||||||
successCount++
|
successCount++
|
||||||
|
|||||||
@@ -365,7 +365,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Temp Unschedulable Rules -->
|
<!-- Temp Unschedulable Rules -->
|
||||||
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600 space-y-4">
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
<label class="input-label mb-0">{{ t('admin.accounts.tempUnschedulable.title') }}</label>
|
||||||
@@ -565,39 +565,74 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
<div>
|
<label class="input-label">{{ t('admin.accounts.expiresAt') }}</label>
|
||||||
<label class="input-label">{{ t('common.status') }}</label>
|
<input v-model="expiresAtInput" type="datetime-local" class="input" />
|
||||||
<Select v-model="form.status" :options="statusOptions" />
|
<p class="input-hint">{{ t('admin.accounts.expiresAtHint') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mixed Scheduling (only for antigravity accounts, read-only in edit mode) -->
|
<div>
|
||||||
<div v-if="account?.platform === 'antigravity'" class="flex items-center gap-2">
|
<div class="flex items-center justify-between">
|
||||||
<label class="flex cursor-not-allowed items-center gap-2 opacity-60">
|
<div>
|
||||||
<input
|
<label class="input-label mb-0">{{
|
||||||
type="checkbox"
|
t('admin.accounts.autoPauseOnExpired')
|
||||||
v-model="mixedScheduling"
|
}}</label>
|
||||||
disabled
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
class="h-4 w-4 cursor-not-allowed rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
{{ t('admin.accounts.autoPauseOnExpiredDesc') }}
|
||||||
/>
|
</p>
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
</div>
|
||||||
{{ t('admin.accounts.mixedScheduling') }}
|
<button
|
||||||
</span>
|
type="button"
|
||||||
</label>
|
@click="autoPauseOnExpired = !autoPauseOnExpired"
|
||||||
<div class="group relative">
|
:class="[
|
||||||
<span
|
'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2',
|
||||||
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
autoPauseOnExpired ? 'bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
?
|
<span
|
||||||
</span>
|
:class="[
|
||||||
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out',
|
||||||
<div
|
autoPauseOnExpired ? 'translate-x-5' : 'translate-x-0'
|
||||||
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
]"
|
||||||
>
|
/>
|
||||||
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t border-gray-200 pt-4 dark:border-dark-600">
|
||||||
|
<div>
|
||||||
|
<label class="input-label">{{ t('common.status') }}</label>
|
||||||
|
<Select v-model="form.status" :options="statusOptions" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mixed Scheduling (only for antigravity accounts, read-only in edit mode) -->
|
||||||
|
<div v-if="account?.platform === 'antigravity'" class="flex items-center gap-2">
|
||||||
|
<label class="flex cursor-not-allowed items-center gap-2 opacity-60">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="mixedScheduling"
|
||||||
|
disabled
|
||||||
|
class="h-4 w-4 cursor-not-allowed rounded border-gray-300 text-primary-500 focus:ring-primary-500 dark:border-dark-500"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
{{ t('admin.accounts.mixedScheduling') }}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="group relative">
|
||||||
|
<span
|
||||||
|
class="inline-flex h-4 w-4 cursor-help items-center justify-center rounded-full bg-gray-200 text-xs text-gray-500 hover:bg-gray-300 dark:bg-dark-600 dark:text-gray-400 dark:hover:bg-dark-500"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</span>
|
||||||
|
<!-- Tooltip(向下显示避免被弹窗裁剪) -->
|
||||||
<div
|
<div
|
||||||
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
class="pointer-events-none absolute left-0 top-full z-[100] mt-1.5 w-72 rounded bg-gray-900 px-3 py-2 text-xs text-white opacity-0 transition-opacity group-hover:opacity-100 dark:bg-gray-700"
|
||||||
></div>
|
>
|
||||||
|
{{ t('admin.accounts.mixedSchedulingTooltip') }}
|
||||||
|
<div
|
||||||
|
class="absolute bottom-full left-3 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -666,6 +701,7 @@ import Icon from '@/components/icons/Icon.vue'
|
|||||||
import ProxySelector from '@/components/common/ProxySelector.vue'
|
import ProxySelector from '@/components/common/ProxySelector.vue'
|
||||||
import GroupSelector from '@/components/common/GroupSelector.vue'
|
import GroupSelector from '@/components/common/GroupSelector.vue'
|
||||||
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
import ModelWhitelistSelector from '@/components/account/ModelWhitelistSelector.vue'
|
||||||
|
import { formatDateTimeLocalInput, parseDateTimeLocalInput } from '@/utils/format'
|
||||||
import {
|
import {
|
||||||
getPresetMappingsByPlatform,
|
getPresetMappingsByPlatform,
|
||||||
commonErrorCodes,
|
commonErrorCodes,
|
||||||
@@ -721,6 +757,7 @@ const customErrorCodesEnabled = ref(false)
|
|||||||
const selectedErrorCodes = ref<number[]>([])
|
const selectedErrorCodes = ref<number[]>([])
|
||||||
const customErrorCodeInput = ref<number | null>(null)
|
const customErrorCodeInput = ref<number | null>(null)
|
||||||
const interceptWarmupRequests = ref(false)
|
const interceptWarmupRequests = ref(false)
|
||||||
|
const autoPauseOnExpired = ref(false)
|
||||||
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
const mixedScheduling = ref(false) // For antigravity accounts: enable mixed scheduling
|
||||||
const tempUnschedEnabled = ref(false)
|
const tempUnschedEnabled = ref(false)
|
||||||
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
const tempUnschedRules = ref<TempUnschedRuleForm[]>([])
|
||||||
@@ -771,7 +808,8 @@ const form = reactive({
|
|||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
priority: 1,
|
priority: 1,
|
||||||
status: 'active' as 'active' | 'inactive',
|
status: 'active' as 'active' | 'inactive',
|
||||||
group_ids: [] as number[]
|
group_ids: [] as number[],
|
||||||
|
expires_at: null as number | null
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusOptions = computed(() => [
|
const statusOptions = computed(() => [
|
||||||
@@ -779,6 +817,13 @@ const statusOptions = computed(() => [
|
|||||||
{ value: 'inactive', label: t('common.inactive') }
|
{ value: 'inactive', label: t('common.inactive') }
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const expiresAtInput = computed({
|
||||||
|
get: () => formatDateTimeLocal(form.expires_at),
|
||||||
|
set: (value: string) => {
|
||||||
|
form.expires_at = parseDateTimeLocal(value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Watchers
|
// Watchers
|
||||||
watch(
|
watch(
|
||||||
() => props.account,
|
() => props.account,
|
||||||
@@ -791,10 +836,12 @@ watch(
|
|||||||
form.priority = newAccount.priority
|
form.priority = newAccount.priority
|
||||||
form.status = newAccount.status as 'active' | 'inactive'
|
form.status = newAccount.status as 'active' | 'inactive'
|
||||||
form.group_ids = newAccount.group_ids || []
|
form.group_ids = newAccount.group_ids || []
|
||||||
|
form.expires_at = newAccount.expires_at ?? null
|
||||||
|
|
||||||
// Load intercept warmup requests setting (applies to all account types)
|
// Load intercept warmup requests setting (applies to all account types)
|
||||||
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
const credentials = newAccount.credentials as Record<string, unknown> | undefined
|
||||||
interceptWarmupRequests.value = credentials?.intercept_warmup_requests === true
|
interceptWarmupRequests.value = credentials?.intercept_warmup_requests === true
|
||||||
|
autoPauseOnExpired.value = newAccount.auto_pause_on_expired === true
|
||||||
|
|
||||||
// Load mixed scheduling setting (only for antigravity accounts)
|
// Load mixed scheduling setting (only for antigravity accounts)
|
||||||
const extra = newAccount.extra as Record<string, unknown> | undefined
|
const extra = newAccount.extra as Record<string, unknown> | undefined
|
||||||
@@ -1042,6 +1089,9 @@ function toPositiveNumber(value: unknown) {
|
|||||||
return Math.trunc(num)
|
return Math.trunc(num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const formatDateTimeLocal = formatDateTimeLocalInput
|
||||||
|
const parseDateTimeLocal = parseDateTimeLocalInput
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
emit('close')
|
emit('close')
|
||||||
@@ -1057,6 +1107,10 @@ const handleSubmit = async () => {
|
|||||||
if (updatePayload.proxy_id === null) {
|
if (updatePayload.proxy_id === null) {
|
||||||
updatePayload.proxy_id = 0
|
updatePayload.proxy_id = 0
|
||||||
}
|
}
|
||||||
|
if (form.expires_at === null) {
|
||||||
|
updatePayload.expires_at = 0
|
||||||
|
}
|
||||||
|
updatePayload.auto_pause_on_expired = autoPauseOnExpired.value
|
||||||
|
|
||||||
// For apikey type, handle credentials update
|
// For apikey type, handle credentials update
|
||||||
if (props.account.type === 'apikey') {
|
if (props.account.type === 'apikey') {
|
||||||
@@ -1097,7 +1151,6 @@ const handleSubmit = async () => {
|
|||||||
if (interceptWarmupRequests.value) {
|
if (interceptWarmupRequests.value) {
|
||||||
newCredentials.intercept_warmup_requests = true
|
newCredentials.intercept_warmup_requests = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applyTempUnschedConfig(newCredentials)) {
|
if (!applyTempUnschedConfig(newCredentials)) {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
return
|
return
|
||||||
@@ -1114,7 +1167,6 @@ const handleSubmit = async () => {
|
|||||||
} else {
|
} else {
|
||||||
delete newCredentials.intercept_warmup_requests
|
delete newCredentials.intercept_warmup_requests
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!applyTempUnschedConfig(newCredentials)) {
|
if (!applyTempUnschedConfig(newCredentials)) {
|
||||||
submitting.value = false
|
submitting.value = false
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1011,6 +1011,7 @@ export default {
|
|||||||
groups: 'Groups',
|
groups: 'Groups',
|
||||||
usageWindows: 'Usage Windows',
|
usageWindows: 'Usage Windows',
|
||||||
lastUsed: 'Last Used',
|
lastUsed: 'Last Used',
|
||||||
|
expiresAt: 'Expires At',
|
||||||
actions: 'Actions'
|
actions: 'Actions'
|
||||||
},
|
},
|
||||||
tempUnschedulable: {
|
tempUnschedulable: {
|
||||||
@@ -1152,11 +1153,16 @@ export default {
|
|||||||
interceptWarmupRequests: 'Intercept Warmup Requests',
|
interceptWarmupRequests: 'Intercept Warmup Requests',
|
||||||
interceptWarmupRequestsDesc:
|
interceptWarmupRequestsDesc:
|
||||||
'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens',
|
'When enabled, warmup requests like title generation will return mock responses without consuming upstream tokens',
|
||||||
|
autoPauseOnExpired: 'Auto Pause On Expired',
|
||||||
|
autoPauseOnExpiredDesc: 'When enabled, the account will auto pause scheduling after it expires',
|
||||||
|
expired: 'Expired',
|
||||||
proxy: 'Proxy',
|
proxy: 'Proxy',
|
||||||
noProxy: 'No Proxy',
|
noProxy: 'No Proxy',
|
||||||
concurrency: 'Concurrency',
|
concurrency: 'Concurrency',
|
||||||
priority: 'Priority',
|
priority: 'Priority',
|
||||||
priorityHint: 'Higher priority accounts are used first',
|
priorityHint: 'Higher priority accounts are used first',
|
||||||
|
expiresAt: 'Expires At',
|
||||||
|
expiresAtHint: 'Leave empty for no expiration',
|
||||||
higherPriorityFirst: 'Higher value means higher priority',
|
higherPriorityFirst: 'Higher value means higher priority',
|
||||||
mixedScheduling: 'Use in /v1/messages',
|
mixedScheduling: 'Use in /v1/messages',
|
||||||
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
|
mixedSchedulingHint: 'Enable to participate in Anthropic/Gemini group scheduling',
|
||||||
|
|||||||
@@ -1061,6 +1061,7 @@ export default {
|
|||||||
groups: '分组',
|
groups: '分组',
|
||||||
usageWindows: '用量窗口',
|
usageWindows: '用量窗口',
|
||||||
lastUsed: '最近使用',
|
lastUsed: '最近使用',
|
||||||
|
expiresAt: '过期时间',
|
||||||
actions: '操作'
|
actions: '操作'
|
||||||
},
|
},
|
||||||
clearRateLimit: '清除速率限制',
|
clearRateLimit: '清除速率限制',
|
||||||
@@ -1286,11 +1287,16 @@ export default {
|
|||||||
errorCodeExists: '该错误码已被选中',
|
errorCodeExists: '该错误码已被选中',
|
||||||
interceptWarmupRequests: '拦截预热请求',
|
interceptWarmupRequests: '拦截预热请求',
|
||||||
interceptWarmupRequestsDesc: '启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token',
|
interceptWarmupRequestsDesc: '启用后,标题生成等预热请求将返回 mock 响应,不消耗上游 token',
|
||||||
|
autoPauseOnExpired: '过期自动暂停调度',
|
||||||
|
autoPauseOnExpiredDesc: '启用后,账号过期将自动暂停调度',
|
||||||
|
expired: '已过期',
|
||||||
proxy: '代理',
|
proxy: '代理',
|
||||||
noProxy: '无代理',
|
noProxy: '无代理',
|
||||||
concurrency: '并发数',
|
concurrency: '并发数',
|
||||||
priority: '优先级',
|
priority: '优先级',
|
||||||
priorityHint: '优先级越高的账号优先使用',
|
priorityHint: '优先级越高的账号优先使用',
|
||||||
|
expiresAt: '过期时间',
|
||||||
|
expiresAtHint: '留空表示不过期',
|
||||||
higherPriorityFirst: '数值越高优先级越高',
|
higherPriorityFirst: '数值越高优先级越高',
|
||||||
mixedScheduling: '在 /v1/messages 中使用',
|
mixedScheduling: '在 /v1/messages 中使用',
|
||||||
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
|
mixedSchedulingHint: '启用后可参与 Anthropic/Gemini 分组的调度',
|
||||||
|
|||||||
@@ -401,6 +401,8 @@ export interface Account {
|
|||||||
status: 'active' | 'inactive' | 'error'
|
status: 'active' | 'inactive' | 'error'
|
||||||
error_message: string | null
|
error_message: string | null
|
||||||
last_used_at: string | null
|
last_used_at: string | null
|
||||||
|
expires_at: number | null
|
||||||
|
auto_pause_on_expired: boolean
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
proxy?: Proxy
|
proxy?: Proxy
|
||||||
@@ -491,6 +493,8 @@ export interface CreateAccountRequest {
|
|||||||
concurrency?: number
|
concurrency?: number
|
||||||
priority?: number
|
priority?: number
|
||||||
group_ids?: number[]
|
group_ids?: number[]
|
||||||
|
expires_at?: number | null
|
||||||
|
auto_pause_on_expired?: boolean
|
||||||
confirm_mixed_channel_risk?: boolean
|
confirm_mixed_channel_risk?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,6 +510,8 @@ export interface UpdateAccountRequest {
|
|||||||
schedulable?: boolean
|
schedulable?: boolean
|
||||||
status?: 'active' | 'inactive'
|
status?: 'active' | 'inactive'
|
||||||
group_ids?: number[]
|
group_ids?: number[]
|
||||||
|
expires_at?: number | null
|
||||||
|
auto_pause_on_expired?: boolean
|
||||||
confirm_mixed_channel_risk?: boolean
|
confirm_mixed_channel_risk?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export function formatBytes(bytes: number, decimals: number = 2): string {
|
|||||||
* 格式化日期
|
* 格式化日期
|
||||||
* @param date 日期字符串或 Date 对象
|
* @param date 日期字符串或 Date 对象
|
||||||
* @param options Intl.DateTimeFormatOptions
|
* @param options Intl.DateTimeFormatOptions
|
||||||
|
* @param localeOverride 可选 locale 覆盖
|
||||||
* @returns 格式化后的日期字符串
|
* @returns 格式化后的日期字符串
|
||||||
*/
|
*/
|
||||||
export function formatDate(
|
export function formatDate(
|
||||||
@@ -108,14 +109,15 @@ export function formatDate(
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: false
|
hour12: false
|
||||||
}
|
},
|
||||||
|
localeOverride?: string
|
||||||
): string {
|
): string {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
|
|
||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
if (isNaN(d.getTime())) return ''
|
if (isNaN(d.getTime())) return ''
|
||||||
|
|
||||||
const locale = getLocale()
|
const locale = localeOverride ?? getLocale()
|
||||||
return new Intl.DateTimeFormat(locale, options).format(d)
|
return new Intl.DateTimeFormat(locale, options).format(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,10 +137,41 @@ export function formatDateOnly(date: string | Date | null | undefined): string {
|
|||||||
/**
|
/**
|
||||||
* 格式化日期时间(完整格式)
|
* 格式化日期时间(完整格式)
|
||||||
* @param date 日期字符串或 Date 对象
|
* @param date 日期字符串或 Date 对象
|
||||||
|
* @param options Intl.DateTimeFormatOptions
|
||||||
|
* @param localeOverride 可选 locale 覆盖
|
||||||
* @returns 格式化后的日期时间字符串
|
* @returns 格式化后的日期时间字符串
|
||||||
*/
|
*/
|
||||||
export function formatDateTime(date: string | Date | null | undefined): string {
|
export function formatDateTime(
|
||||||
return formatDate(date)
|
date: string | Date | null | undefined,
|
||||||
|
options?: Intl.DateTimeFormatOptions,
|
||||||
|
localeOverride?: string
|
||||||
|
): string {
|
||||||
|
return formatDate(date, options, localeOverride)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化为 datetime-local 控件值(YYYY-MM-DDTHH:mm,使用本地时间)
|
||||||
|
*/
|
||||||
|
export function formatDateTimeLocalInput(timestampSeconds: number | null): string {
|
||||||
|
if (!timestampSeconds) return ''
|
||||||
|
const date = new Date(timestampSeconds * 1000)
|
||||||
|
if (isNaN(date.getTime())) return ''
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}T${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 datetime-local 控件值为时间戳(秒,使用本地时间)
|
||||||
|
*/
|
||||||
|
export function parseDateTimeLocalInput(value: string): number | null {
|
||||||
|
if (!value) return null
|
||||||
|
const date = new Date(value)
|
||||||
|
if (isNaN(date.getTime())) return null
|
||||||
|
return Math.floor(date.getTime() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,6 +70,25 @@
|
|||||||
<template #cell-last_used_at="{ value }">
|
<template #cell-last_used_at="{ value }">
|
||||||
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatRelativeTime(value) }}</span>
|
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatRelativeTime(value) }}</span>
|
||||||
</template>
|
</template>
|
||||||
|
<template #cell-expires_at="{ row, value }">
|
||||||
|
<div class="flex flex-col items-start gap-1">
|
||||||
|
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatExpiresAt(value) }}</span>
|
||||||
|
<div v-if="isExpired(value) || (row.auto_pause_on_expired && value)" class="flex items-center gap-1">
|
||||||
|
<span
|
||||||
|
v-if="isExpired(value)"
|
||||||
|
class="inline-flex items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.expired') }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="row.auto_pause_on_expired && value"
|
||||||
|
class="inline-flex items-center rounded-md bg-emerald-100 px-2 py-0.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300"
|
||||||
|
>
|
||||||
|
{{ t('admin.accounts.autoPauseOnExpired') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template #cell-actions="{ row }">
|
<template #cell-actions="{ row }">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<button @click="handleEdit(row)" class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400">
|
<button @click="handleEdit(row)" class="flex flex-col items-center gap-0.5 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-primary-600 dark:hover:bg-dark-700 dark:hover:text-primary-400">
|
||||||
@@ -128,7 +147,7 @@ import AccountUsageCell from '@/components/account/AccountUsageCell.vue'
|
|||||||
import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue'
|
import AccountTodayStatsCell from '@/components/account/AccountTodayStatsCell.vue'
|
||||||
import GroupBadge from '@/components/common/GroupBadge.vue'
|
import GroupBadge from '@/components/common/GroupBadge.vue'
|
||||||
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
import PlatformTypeBadge from '@/components/common/PlatformTypeBadge.vue'
|
||||||
import { formatRelativeTime } from '@/utils/format'
|
import { formatDateTime, formatRelativeTime } from '@/utils/format'
|
||||||
import type { Account, Proxy, Group } from '@/types'
|
import type { Account, Proxy, Group } from '@/types'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@@ -178,6 +197,7 @@ const cols = computed(() => {
|
|||||||
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
{ key: 'usage', label: t('admin.accounts.columns.usageWindows'), sortable: false },
|
||||||
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
{ key: 'priority', label: t('admin.accounts.columns.priority'), sortable: true },
|
||||||
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
{ key: 'last_used_at', label: t('admin.accounts.columns.lastUsed'), sortable: true },
|
||||||
|
{ key: 'expires_at', label: t('admin.accounts.columns.expiresAt'), sortable: true },
|
||||||
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
|
{ key: 'notes', label: t('admin.accounts.columns.notes'), sortable: false },
|
||||||
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
{ key: 'actions', label: t('admin.accounts.columns.actions'), sortable: false }
|
||||||
)
|
)
|
||||||
@@ -204,6 +224,25 @@ const confirmDelete = async () => { if(!deletingAcc.value) return; try { await a
|
|||||||
const handleToggleSchedulable = async (a: Account) => { togglingSchedulable.value = a.id; try { await adminAPI.accounts.setSchedulable(a.id, !a.schedulable); load() } finally { togglingSchedulable.value = null } }
|
const handleToggleSchedulable = async (a: Account) => { togglingSchedulable.value = a.id; try { await adminAPI.accounts.setSchedulable(a.id, !a.schedulable); load() } finally { togglingSchedulable.value = null } }
|
||||||
const handleShowTempUnsched = (a: Account) => { tempUnschedAcc.value = a; showTempUnsched.value = true }
|
const handleShowTempUnsched = (a: Account) => { tempUnschedAcc.value = a; showTempUnsched.value = true }
|
||||||
const handleTempUnschedReset = async () => { if(!tempUnschedAcc.value) return; try { await adminAPI.accounts.clearError(tempUnschedAcc.value.id); showTempUnsched.value = false; tempUnschedAcc.value = null; load() } catch (error) { console.error('Failed to reset temp unscheduled:', error) } }
|
const handleTempUnschedReset = async () => { if(!tempUnschedAcc.value) return; try { await adminAPI.accounts.clearError(tempUnschedAcc.value.id); showTempUnsched.value = false; tempUnschedAcc.value = null; load() } catch (error) { console.error('Failed to reset temp unscheduled:', error) } }
|
||||||
|
const formatExpiresAt = (value: number | null) => {
|
||||||
|
if (!value) return '-'
|
||||||
|
return formatDateTime(
|
||||||
|
new Date(value * 1000),
|
||||||
|
{
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
},
|
||||||
|
'sv-SE'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const isExpired = (value: number | null) => {
|
||||||
|
if (!value) return false
|
||||||
|
return value * 1000 <= Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) } })
|
onMounted(async () => { load(); try { const [p, g] = await Promise.all([adminAPI.proxies.getAll(), adminAPI.groups.getAll()]); proxies.value = p; groups.value = g } catch (error) { console.error('Failed to load proxies/groups:', error) } })
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
import checker from 'vite-plugin-checker'
|
import checker from 'vite-plugin-checker'
|
||||||
import { resolve } from 'path'
|
import { resolve } from 'path'
|
||||||
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
@@ -29,7 +30,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 3000,
|
port: Number(process.env.VITE_DEV_PORT || 3000),
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080',
|
target: process.env.VITE_DEV_PROXY_TARGET || 'http://localhost:8080',
|
||||||
|
|||||||
Reference in New Issue
Block a user