From 79e100a1cf011326aa55f816f889608d325ac52f Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 19 Apr 2026 20:18:38 +0800 Subject: [PATCH] fix: delete scheduled test plans when account is deleted Accounts use soft-delete (setting deleted_at), so PostgreSQL's ON DELETE CASCADE on scheduled_test_plans.account_id never fires. This leaves orphaned plans that continue executing and cannot be managed from the frontend since the account no longer exists. Closes Wei-Shaw/sub2api#1728 --- backend/cmd/server/wire_gen.go | 4 +- .../repository/scheduled_test_repo.go | 5 ++ backend/internal/service/admin_service.go | 74 +++++++++++-------- .../internal/service/scheduled_test_port.go | 1 + 4 files changed, 50 insertions(+), 34 deletions(-) diff --git a/backend/cmd/server/wire_gen.go b/backend/cmd/server/wire_gen.go index 1d39fa1e31..2a2c94727a 100644 --- a/backend/cmd/server/wire_gen.go +++ b/backend/cmd/server/wire_gen.go @@ -104,7 +104,8 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { proxyExitInfoProber := repository.NewProxyExitInfoProber(configConfig) proxyLatencyCache := repository.NewProxyLatencyCache(redisClient) privacyClientFactory := providePrivacyClientFactory() - adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory) + scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db) + adminService := service.NewAdminService(userRepository, groupRepository, accountRepository, proxyRepository, apiKeyRepository, redeemCodeRepository, userGroupRateRepository, billingCacheService, proxyExitInfoProber, proxyLatencyCache, apiKeyAuthCacheInvalidator, client, settingService, subscriptionService, userSubscriptionRepository, privacyClientFactory, scheduledTestPlanRepository) concurrencyCache := repository.ProvideConcurrencyCache(redisClient, configConfig) concurrencyService := service.ProvideConcurrencyService(concurrencyCache, accountRepository, configConfig) adminUserHandler := admin.NewUserHandler(adminService, concurrencyService) @@ -205,7 +206,6 @@ func initializeApplication(buildInfo handler.BuildInfo) (*Application, error) { errorPassthroughHandler := admin.NewErrorPassthroughHandler(errorPassthroughService) tlsFingerprintProfileHandler := admin.NewTLSFingerprintProfileHandler(tlsFingerprintProfileService) adminAPIKeyHandler := admin.NewAdminAPIKeyHandler(adminService) - scheduledTestPlanRepository := repository.NewScheduledTestPlanRepository(db) scheduledTestResultRepository := repository.NewScheduledTestResultRepository(db) scheduledTestService := service.ProvideScheduledTestService(scheduledTestPlanRepository, scheduledTestResultRepository) scheduledTestHandler := admin.NewScheduledTestHandler(scheduledTestService) diff --git a/backend/internal/repository/scheduled_test_repo.go b/backend/internal/repository/scheduled_test_repo.go index c03d1df90f..1afadad20e 100644 --- a/backend/internal/repository/scheduled_test_repo.go +++ b/backend/internal/repository/scheduled_test_repo.go @@ -77,6 +77,11 @@ func (r *scheduledTestPlanRepository) Delete(ctx context.Context, id int64) erro return err } +func (r *scheduledTestPlanRepository) DeleteByAccountID(ctx context.Context, accountID int64) error { + _, err := r.db.ExecContext(ctx, `DELETE FROM scheduled_test_plans WHERE account_id = $1`, accountID) + return err +} + func (r *scheduledTestPlanRepository) UpdateAfterRun(ctx context.Context, id int64, lastRunAt time.Time, nextRunAt time.Time) error { _, err := r.db.ExecContext(ctx, ` UPDATE scheduled_test_plans SET last_run_at = $2, next_run_at = $3, updated_at = NOW() WHERE id = $1 diff --git a/backend/internal/service/admin_service.go b/backend/internal/service/admin_service.go index 701f3659ee..3c286b44e1 100644 --- a/backend/internal/service/admin_service.go +++ b/backend/internal/service/admin_service.go @@ -423,22 +423,23 @@ const ( // adminServiceImpl implements AdminService type adminServiceImpl struct { - userRepo UserRepository - groupRepo GroupRepository - accountRepo AccountRepository - proxyRepo ProxyRepository - apiKeyRepo APIKeyRepository - redeemCodeRepo RedeemCodeRepository - userGroupRateRepo UserGroupRateRepository - billingCacheService *BillingCacheService - proxyProber ProxyExitInfoProber - proxyLatencyCache ProxyLatencyCache - authCacheInvalidator APIKeyAuthCacheInvalidator - entClient *dbent.Client // 用于开启数据库事务 - settingService *SettingService - defaultSubAssigner DefaultSubscriptionAssigner - userSubRepo UserSubscriptionRepository - privacyClientFactory PrivacyClientFactory + userRepo UserRepository + groupRepo GroupRepository + accountRepo AccountRepository + proxyRepo ProxyRepository + apiKeyRepo APIKeyRepository + redeemCodeRepo RedeemCodeRepository + userGroupRateRepo UserGroupRateRepository + billingCacheService *BillingCacheService + proxyProber ProxyExitInfoProber + proxyLatencyCache ProxyLatencyCache + authCacheInvalidator APIKeyAuthCacheInvalidator + entClient *dbent.Client // 用于开启数据库事务 + settingService *SettingService + defaultSubAssigner DefaultSubscriptionAssigner + userSubRepo UserSubscriptionRepository + privacyClientFactory PrivacyClientFactory + scheduledTestPlanRepo ScheduledTestPlanRepository } type userGroupRateBatchReader interface { @@ -463,24 +464,26 @@ func NewAdminService( defaultSubAssigner DefaultSubscriptionAssigner, userSubRepo UserSubscriptionRepository, privacyClientFactory PrivacyClientFactory, + scheduledTestPlanRepo ScheduledTestPlanRepository, ) AdminService { return &adminServiceImpl{ - userRepo: userRepo, - groupRepo: groupRepo, - accountRepo: accountRepo, - proxyRepo: proxyRepo, - apiKeyRepo: apiKeyRepo, - redeemCodeRepo: redeemCodeRepo, - userGroupRateRepo: userGroupRateRepo, - billingCacheService: billingCacheService, - proxyProber: proxyProber, - proxyLatencyCache: proxyLatencyCache, - authCacheInvalidator: authCacheInvalidator, - entClient: entClient, - settingService: settingService, - defaultSubAssigner: defaultSubAssigner, - userSubRepo: userSubRepo, - privacyClientFactory: privacyClientFactory, + userRepo: userRepo, + groupRepo: groupRepo, + accountRepo: accountRepo, + proxyRepo: proxyRepo, + apiKeyRepo: apiKeyRepo, + redeemCodeRepo: redeemCodeRepo, + userGroupRateRepo: userGroupRateRepo, + billingCacheService: billingCacheService, + proxyProber: proxyProber, + proxyLatencyCache: proxyLatencyCache, + authCacheInvalidator: authCacheInvalidator, + entClient: entClient, + settingService: settingService, + defaultSubAssigner: defaultSubAssigner, + userSubRepo: userSubRepo, + privacyClientFactory: privacyClientFactory, + scheduledTestPlanRepo: scheduledTestPlanRepo, } } @@ -1860,6 +1863,13 @@ func (s *adminServiceImpl) BulkUpdateAccounts(ctx context.Context, input *BulkUp } func (s *adminServiceImpl) DeleteAccount(ctx context.Context, id int64) error { + if err := s.scheduledTestPlanRepo.DeleteByAccountID(ctx, id); err != nil { + slog.Error("failed to delete scheduled test plans for account", + "account_id", id, + "error", err, + ) + return fmt.Errorf("delete scheduled test plans: %w", err) + } if err := s.accountRepo.Delete(ctx, id); err != nil { return err } diff --git a/backend/internal/service/scheduled_test_port.go b/backend/internal/service/scheduled_test_port.go index 1c0fdf218f..22c9cdb8db 100644 --- a/backend/internal/service/scheduled_test_port.go +++ b/backend/internal/service/scheduled_test_port.go @@ -41,6 +41,7 @@ type ScheduledTestPlanRepository interface { ListDue(ctx context.Context, now time.Time) ([]*ScheduledTestPlan, error) Update(ctx context.Context, plan *ScheduledTestPlan) (*ScheduledTestPlan, error) Delete(ctx context.Context, id int64) error + DeleteByAccountID(ctx context.Context, accountID int64) error UpdateAfterRun(ctx context.Context, id int64, lastRunAt time.Time, nextRunAt time.Time) error }