package service import ( "context" "log/slog" "net/http" "time" "schedule_converter/auth" "schedule_converter/captcha" "schedule_converter/client" "schedule_converter/config" "schedule_converter/models" "schedule_converter/parser" apperr "schedule_converter/pkg/errors" ) type ScheduleService struct { httpClient *http.Client maxRetries int retryDelay time.Duration } type ScheduleOption func(*ScheduleService) func WithMaxRetries(n int) ScheduleOption { return func(s *ScheduleService) { s.maxRetries = n } } func WithRetryDelay(d time.Duration) ScheduleOption { return func(s *ScheduleService) { s.retryDelay = d } } func NewScheduleService(opts ...ScheduleOption) *ScheduleService { s := &ScheduleService{ httpClient: client.Client, maxRetries: 3, retryDelay: 3 * time.Second, } for _, opt := range opts { opt(s) } return s } func NewScheduleServiceWithClient(httpClient *http.Client, opts ...ScheduleOption) *ScheduleService { s := &ScheduleService{ httpClient: httpClient, maxRetries: 3, retryDelay: 3 * time.Second, } for _, opt := range opts { opt(s) } return s } func (s *ScheduleService) FetchSchedule(ctx context.Context, username, password, semester string) ([]models.Course, error) { log := slog.With("username", username, "semester", semester) if err := client.GetInitialCookiesWithClient(s.httpClient); err != nil { return nil, apperr.New("fetch_schedule", err, "获取Cookie失败") } var lastErr error for attempt := 1; attempt <= s.maxRetries; attempt++ { select { case <-ctx.Done(): return nil, ctx.Err() default: } log.Info("尝试登录", "attempt", attempt, "max_retries", s.maxRetries) captchaCode, err := captcha.GetAndRecognize(s.httpClient) if err != nil { log.Warn("验证码获取失败", "error", err, "attempt", attempt) lastErr = err time.Sleep(s.retryDelay) continue } success, err := auth.LoginWithClient(s.httpClient, captchaCode, username, password) if err != nil { log.Warn("登录失败", "error", err, "attempt", attempt) lastErr = err time.Sleep(s.retryDelay) continue } if success { if semester == "" { semester = config.Semester } courses, err := parser.FetchScheduleWithClient(s.httpClient, semester) if err != nil { return nil, apperr.New("fetch_schedule", err, "获取课表失败") } log.Info("获取课表成功", "course_count", len(courses)) return courses, nil } log.Warn("登录验证失败,稍后重试", "attempt", attempt) time.Sleep(s.retryDelay) } return nil, apperr.New("fetch_schedule", lastErr, "登录失败,已重试") } func (s *ScheduleService) FetchDefaultSchedule(ctx context.Context) ([]models.Course, error) { return s.FetchSchedule(ctx, config.Username, config.Password, config.Semester) } func (s *ScheduleService) FetchScheduleForUser(username, password, semester string) ([]models.Course, error) { return s.FetchSchedule(context.Background(), username, password, semester) } func (s *ScheduleService) FetchDefaultScheduleLegacy() ([]models.Course, error) { return s.FetchScheduleForUser(config.Username, config.Password, config.Semester) }