package notification import ( "context" "time" "go.uber.org/zap" "gorm.io/gorm" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // CleanupService 按通知类别的保留期限分批删除过期通知事实。 type CleanupService struct { db *gorm.DB logger *zap.Logger now func() time.Time } // NewCleanupService 创建通知保留清理服务。 func NewCleanupService(db *gorm.DB, logger *zap.Logger) *CleanupService { if logger == nil { logger = zap.NewNop() } return &CleanupService{db: db, logger: logger, now: time.Now} } // Run 按类别、创建时间和稳定主键执行有界分批清理。 func (s *CleanupService) Run(ctx context.Context) error { policies := []struct { category string days int }{ {constants.NotificationCategoryApproval, constants.NotificationApprovalRetentionDays}, {constants.NotificationCategoryExpiry, constants.NotificationExpiryRetentionDays}, {constants.NotificationCategorySync, constants.NotificationSyncRetentionDays}, {constants.NotificationCategorySystem, constants.NotificationSystemRetentionDays}, } for _, policy := range policies { deleted, err := s.cleanupCategory(ctx, policy.category, s.now().UTC().AddDate(0, 0, -policy.days)) if err != nil { return err } s.logger.Info("站内通知保留清理完成", zap.String("category", policy.category), zap.Int64("deleted_count", deleted)) } return nil } func (s *CleanupService) cleanupCategory(ctx context.Context, category string, cutoff time.Time) (int64, error) { var total int64 for batch := 0; batch < constants.NotificationCleanupMaxBatches; batch++ { result := s.db.WithContext(ctx).Exec(`WITH candidates AS ( SELECT id FROM tb_notification WHERE category = ? AND created_at < ? ORDER BY created_at ASC, id ASC LIMIT ? ) DELETE FROM tb_notification AS notification USING candidates WHERE notification.id = candidates.id`, category, cutoff, constants.NotificationCleanupBatchSize) if result.Error != nil { return total, errors.Wrap(errors.CodeDatabaseError, result.Error, "清理站内通知失败") } total += result.RowsAffected if result.RowsAffected < constants.NotificationCleanupBatchSize { break } } return total, nil }