package service import ( "context" "log/slog" "net/http" "net/http/cookiejar" "net/url" "time" "schedule_converter/auth" "schedule_converter/captcha" "schedule_converter/client" "schedule_converter/config" apperr "schedule_converter/pkg/errors" ) const ( maxLoginRetries = 3 loginRetryInterval = 3 * time.Second ) // Service 封装教务系统的登录与数据抓取编排逻辑。 type Service struct{} // NewService 创建 service。 func NewService() *Service { return &Service{} } // newAuthenticatedClient 创建带 cookie jar 的 HTTP 客户端,设置 TWFID cookie 并完成登录。 func (s *Service) newAuthenticatedClient(ctx context.Context, username, password, twfid string) (*http.Client, error) { jar, err := cookiejar.New(nil) if err != nil { return nil, apperr.New("create_jar", err, "创建 cookie jar 失败") } httpClient := &http.Client{Jar: jar} if twfid == "" { twfid = config.Get().TWFID } s.setTWFIDCookie(httpClient, config.Get().BaseURL, twfid) if err := s.login(ctx, httpClient, username, password); err != nil { return nil, err } return httpClient, nil } func (s *Service) setTWFIDCookie(httpClient *http.Client, baseURL, twfid string) { u, err := url.Parse(baseURL) if err != nil { slog.Warn("解析 baseURL 失败,无法设置 TWFID cookie", "base_url", baseURL, "error", err) return } httpClient.Jar.SetCookies(u, []*http.Cookie{ {Name: "TWFID", Value: twfid}, }) } // login 执行登录流程,包含验证码识别与重试。 func (s *Service) login(ctx context.Context, httpClient *http.Client, username, password string) error { log := slog.With("username", username) if err := client.GetInitialCookiesWithClient(httpClient); err != nil { return apperr.New("get_cookies", err, "获取初始 Cookie 失败") } for attempt := 1; attempt <= maxLoginRetries; attempt++ { select { case <-ctx.Done(): return ctx.Err() default: } log.Info("尝试登录", "attempt", attempt, "max_retries", maxLoginRetries) captchaCode, err := captcha.GetAndRecognize(httpClient) if err != nil { log.Warn("验证码获取失败", "error", err, "attempt", attempt) time.Sleep(loginRetryInterval) continue } success, err := auth.LoginWithClient(ctx, httpClient, captchaCode, username, password) if err != nil { log.Warn("登录请求失败", "error", err, "attempt", attempt) time.Sleep(loginRetryInterval) continue } if success { return nil } log.Warn("登录验证失败", "attempt", attempt) time.Sleep(loginRetryInterval) } return apperr.ErrLoginFailed }