From c8fe2f6516b842df78b0b1eaf5989b27d2b52c3c Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Sat, 27 Jun 2026 15:14:43 +0800 Subject: [PATCH] fix active subscription visibility after updates --- .../repository/subscription_repository.go | 6 +- internal/service/subscription.go | 17 +++- internal/service/subscription_archive.go | 10 +-- internal/service/subscription_archive_test.go | 47 +++++++++++ internal/service/subscription_test.go | 78 +++++++++++++++++++ 5 files changed, 149 insertions(+), 9 deletions(-) diff --git a/internal/repository/subscription_repository.go b/internal/repository/subscription_repository.go index 0276cd7..fe3a610 100644 --- a/internal/repository/subscription_repository.go +++ b/internal/repository/subscription_repository.go @@ -21,7 +21,11 @@ func (r *SubscriptionRepository) Create(ctx context.Context, s *model.Subscripti // intentionally excluded from scheduler polling and the active management list. func (r *SubscriptionRepository) List(ctx context.Context) ([]model.Subscription, error) { var rows []model.Subscription - err := r.db.WithContext(ctx).Where("archived_at IS NULL").Order("created_at desc").Find(&rows).Error + err := r.db.WithContext(ctx).Unscoped(). + Where("archived_at IS NULL"). + Where("deleted_at IS NULL OR enabled = ?", true). + Order("created_at desc"). + Find(&rows).Error return rows, err } diff --git a/internal/service/subscription.go b/internal/service/subscription.go index f884834..38506f4 100644 --- a/internal/service/subscription.go +++ b/internal/service/subscription.go @@ -138,7 +138,12 @@ func (s *SubscriptionService) Delete(ctx context.Context, id string) error { if !errors.Is(err, gorm.ErrRecordNotFound) { return err } - return s.repo.DB.WithContext(ctx).Where("id = ?", id).Delete(&model.Subscription{}).Error + if err := s.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", id).First(&sub).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } } if err := s.deleteSubscriptionDownloads(ctx, &sub); err != nil { return err @@ -146,7 +151,15 @@ func (s *SubscriptionService) Delete(ctx context.Context, id string) error { if s.repo.Setting != nil { _ = s.repo.Setting.Delete(ctx, fmt.Sprintf("subscription.%s.seen", id)) } - return s.repo.DB.Where("id = ?", id).Delete(&model.Subscription{}).Error + return s.repo.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := tx.Unscoped().Model(&model.Subscription{}).Where("id = ?", id).Update("enabled", false).Error; err != nil { + return err + } + if sub.DeletedAt.Valid { + return nil + } + return tx.Where("id = ?", id).Delete(&model.Subscription{}).Error + }) } // RunNow forces a poll for one subscription, ignoring its schedule. Used diff --git a/internal/service/subscription_archive.go b/internal/service/subscription_archive.go index 8656c53..702c5c6 100644 --- a/internal/service/subscription_archive.go +++ b/internal/service/subscription_archive.go @@ -20,14 +20,16 @@ func (s *SubscriptionService) History(ctx context.Context) ([]model.Subscription // rule can match resources again when it is run next. func (s *SubscriptionService) Restore(ctx context.Context, id string) (*model.Subscription, error) { var sub model.Subscription - if err := s.repo.DB.WithContext(ctx).Where("id = ?", id).First(&sub).Error; err != nil { + if err := s.repo.DB.WithContext(ctx).Unscoped().Where("id = ?", id).First(&sub).Error; err != nil { return nil, err } - if err := s.repo.DB.WithContext(ctx).Model(&model.Subscription{}). + if err := s.repo.DB.WithContext(ctx).Unscoped().Model(&model.Subscription{}). Where("id = ?", id). Updates(map[string]any{ "enabled": true, + "archived_at": nil, "archive_reason": "", + "deleted_at": nil, // 重置为 0:此前可能被 feed 低估并锁死(updateSubscriptionTotalEpisodes // 只增不减,resolveSubscriptionTotalEpisodes 见 >0 即不再回查元数据)。 // 归零后下次 run 会从 TMDb/豆瓣等权威源重算真实总集数,避免恢复后 @@ -36,10 +38,6 @@ func (s *SubscriptionService) Restore(ctx context.Context, id string) (*model.Su }).Error; err != nil { return nil, err } - if err := s.repo.DB.WithContext(ctx). - Exec("UPDATE subscriptions SET archived_at = NULL WHERE id = ?", id).Error; err != nil { - return nil, err - } if s.repo.Setting != nil { _ = s.repo.Setting.Delete(ctx, fmt.Sprintf("subscription.%s.seen", id)) } diff --git a/internal/service/subscription_archive_test.go b/internal/service/subscription_archive_test.go index b1ca7af..a0f0999 100644 --- a/internal/service/subscription_archive_test.go +++ b/internal/service/subscription_archive_test.go @@ -268,3 +268,50 @@ func TestRestoreArchivedSubscriptionReturnsToActiveAndClearsSeenState(t *testing t.Fatalf("seen state = %q, want cleared", seen) } } + +func TestRestoreSoftDeletedArchivedSubscriptionReturnsToActive(t *testing.T) { + db := newServiceTestDB(t, &model.Subscription{}, &model.Setting{}) + repos := repository.New(db) + svc := NewSubscriptionService(nil, zap.NewNop(), repos, nil, nil, NewHub(zap.NewNop())) + sub := &model.Subscription{ + Name: "Legacy Hidden History 自动订阅", + FeedURL: "https://rss.example/feed", + Filter: "Legacy Hidden History", + MediaType: "tv", + TotalEpisodes: 12, + } + if err := repos.Subscription.Create(t.Context(), sub); err != nil { + t.Fatal(err) + } + archivedAt := time.Now() + if err := repos.Subscription.Archive(t.Context(), sub.ID, "订阅完成:12/12", archivedAt); err != nil { + t.Fatal(err) + } + if err := db.Where("id = ?", sub.ID).Delete(&model.Subscription{}).Error; err != nil { + t.Fatal(err) + } + + restored, err := svc.Restore(t.Context(), sub.ID) + if err != nil { + t.Fatal(err) + } + if restored.ArchivedAt != nil || restored.ArchiveReason != "" || !restored.Enabled || restored.TotalEpisodes != 0 { + t.Fatalf("restored subscription not reset: %#v", restored) + } + active, err := repos.Subscription.List(t.Context()) + if err != nil { + t.Fatal(err) + } + if len(active) != 1 || active[0].ID != sub.ID { + t.Fatalf("active subscriptions = %#v, want restored legacy subscription", active) + } + var deletedCount int64 + if err := db.Unscoped().Model(&model.Subscription{}). + Where("id = ? AND deleted_at IS NOT NULL", sub.ID). + Count(&deletedCount).Error; err != nil { + t.Fatal(err) + } + if deletedCount != 0 { + t.Fatal("restored subscription kept deleted_at set") + } +} diff --git a/internal/service/subscription_test.go b/internal/service/subscription_test.go index b715f0a..a2b50f1 100644 --- a/internal/service/subscription_test.go +++ b/internal/service/subscription_test.go @@ -90,6 +90,20 @@ func TestDeleteSubscriptionRemovesDownloaderTaskAndSeenState(t *testing.T) { if count != 0 { t.Fatalf("active subscription count = %d, want 0", count) } + var deleted model.Subscription + if err := db.Unscoped().Where("id = ?", sub.ID).First(&deleted).Error; err != nil { + t.Fatal(err) + } + if deleted.Enabled { + t.Fatal("deleted subscription stayed enabled; active legacy compatibility would show it again") + } + active, err := repos.Subscription.List(t.Context()) + if err != nil { + t.Fatal(err) + } + if len(active) != 0 { + t.Fatalf("active subscriptions = %#v, want deleted subscription hidden", active) + } } func TestDeletedDownloadTaskDoesNotBlockSubscriptionReadd(t *testing.T) { @@ -100,3 +114,67 @@ func TestDeletedDownloadTaskDoesNotBlockSubscriptionReadd(t *testing.T) { t.Fatal("removed download task must not block subscription re-add") } } + +func TestListIncludesEnabledSoftDeletedActiveSubscription(t *testing.T) { + db := newServiceTestDB(t, &model.Subscription{}) + repos := repository.New(db) + sub := &model.Subscription{ + Name: "Hidden Active 自动订阅", + FeedURL: "site-search://search?keyword=Hidden%20Active", + Filter: "Hidden Active", + Enabled: true, + } + if err := repos.Subscription.Create(t.Context(), sub); err != nil { + t.Fatal(err) + } + if err := db.Where("id = ?", sub.ID).Delete(&model.Subscription{}).Error; err != nil { + t.Fatal(err) + } + + active, err := repos.Subscription.List(t.Context()) + if err != nil { + t.Fatal(err) + } + if len(active) != 1 || active[0].ID != sub.ID { + t.Fatalf("active subscriptions = %#v, want soft-deleted enabled subscription recovered", active) + } +} + +func TestDeleteRecoveredSoftDeletedSubscriptionClearsSeenAndHidesIt(t *testing.T) { + db := newServiceTestDB(t, &model.Subscription{}, &model.Setting{}, &model.DownloadTask{}) + repos := repository.New(db) + svc := NewSubscriptionService(nil, zap.NewNop(), repos, nil, nil, NewHub(zap.NewNop())) + sub := &model.Subscription{ + Name: "Recovered Hidden 自动订阅", + FeedURL: "site-search://search?keyword=Recovered%20Hidden", + Filter: "Recovered Hidden", + Enabled: true, + } + if err := repos.Subscription.Create(t.Context(), sub); err != nil { + t.Fatal(err) + } + if err := repos.Setting.Set(t.Context(), "subscription."+sub.ID+".seen", "old-guid"); err != nil { + t.Fatal(err) + } + if err := db.Where("id = ?", sub.ID).Delete(&model.Subscription{}).Error; err != nil { + t.Fatal(err) + } + + if err := svc.Delete(t.Context(), sub.ID); err != nil { + t.Fatal(err) + } + active, err := repos.Subscription.List(t.Context()) + if err != nil { + t.Fatal(err) + } + if len(active) != 0 { + t.Fatalf("active subscriptions = %#v, want recovered deleted subscription hidden", active) + } + seen, err := repos.Setting.Get(t.Context(), "subscription."+sub.ID+".seen") + if err != nil { + t.Fatal(err) + } + if seen != "" { + t.Fatalf("seen state = %q, want cleared", seen) + } +}