// Package invite runs asynchronous invite validity consumers. package invite import ( "context" "log/slog" "time" "hyapp/pkg/logx" invitedomain "hyapp/services/user-service/internal/domain/invite" ) // Repository applies wallet recharge events to user-service invite progress. type Repository interface { ApplyRechargeEvent(ctx context.Context, event invitedomain.RechargeEvent) (invitedomain.RechargeApplyResult, error) } // WalletOutboxReader scans wallet recharge facts in creation order. type WalletOutboxReader interface { ListRechargeEventsAfter(ctx context.Context, cursor invitedomain.WalletRechargeCursor, batchSize int) ([]invitedomain.RechargeEvent, error) } // Service owns the invite validity event consumer. type Service struct { repository Repository reader WalletOutboxReader } // WorkerOptions controls local wallet outbox polling. type WorkerOptions struct { WorkerID string PollInterval time.Duration BatchSize int } // New creates an invite service with persistence and wallet outbox reader dependencies. func New(repository Repository, reader WalletOutboxReader) *Service { return &Service{repository: repository, reader: reader} } // RunWalletRechargeWorker consumes WalletRechargeRecorded facts and advances valid_invite_count. func (s *Service) RunWalletRechargeWorker(ctx context.Context, options WorkerOptions) { if s == nil || s.repository == nil || s.reader == nil { logx.Warn(ctx, "worker_disabled", slog.String("worker", "wallet_recharge"), slog.String("component", "user_invite"), slog.String("reason", "missing_dependency")) return } if options.PollInterval <= 0 { options.PollInterval = 5 * time.Second } if options.BatchSize <= 0 { options.BatchSize = 100 } var cursor invitedomain.WalletRechargeCursor for { processed, err := s.processBatch(ctx, &cursor, options.BatchSize) if err != nil { logx.Error(ctx, "worker_batch_failed", err, slog.String("worker", "wallet_recharge"), slog.String("component", "user_invite"), slog.String("worker_id", options.WorkerID), slog.Int("batch_size", options.BatchSize)) } if ctx.Err() != nil { return } if processed >= options.BatchSize { // 有积压时不 sleep,直到追上 wallet_outbox 当前末尾。 continue } select { case <-ctx.Done(): return case <-time.After(options.PollInterval): } } } func (s *Service) processBatch(ctx context.Context, cursor *invitedomain.WalletRechargeCursor, batchSize int) (int, error) { events, err := s.reader.ListRechargeEventsAfter(ctx, *cursor, batchSize) if err != nil { return 0, err } for _, event := range events { if _, err := s.repository.ApplyRechargeEvent(ctx, event); err != nil { return 0, err } cursor.CreatedAtMs = event.OccurredAtMs cursor.EventID = event.EventID } return len(events), nil }