fix active subscription visibility after updates

This commit is contained in:
ShukeBta
2026-06-27 15:14:43 +08:00
parent f88b833cbc
commit c8fe2f6516
5 changed files with 149 additions and 9 deletions
@@ -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
}
+15 -2
View File
@@ -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
+4 -6
View File
@@ -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))
}
@@ -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")
}
}
+78
View File
@@ -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)
}
}