package grpc import ( "context" "encoding/json" "log/slog" "net/http" "net/http/cookiejar" "net/url" "time" "schedule_converter/auth" "schedule_converter/captcha" "schedule_converter/client" "schedule_converter/config" "schedule_converter/models" "schedule_converter/parser" apperr "schedule_converter/pkg/errors" ) const ( maxLoginRetries = 3 loginRetryInterval = 3 * time.Second ) func createHTTPClient() (*http.Client, error) { jar, err := cookiejar.New(nil) if err != nil { return nil, apperr.New("create_jar", err, "创建 cookie jar 失败") } return &http.Client{Jar: jar}, nil } func setTWFIDCookie(httpClient *http.Client, baseURL, twfid string) { cfg := config.Get() if twfid == "" { twfid = cfg.TWFID } 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}, }) } func executeLogin(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(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 } func executeLoginAndFetchSchedule(ctx context.Context, httpClient *http.Client, username, password, semester string) ([]models.Course, error) { if err := executeLogin(ctx, httpClient, username, password); err != nil { return nil, err } cfg := config.Get() if semester == "" { semester = cfg.Semester } return parser.FetchScheduleWithClient(httpClient, semester) } func unmarshalPayload(payload []byte, v any) error { if err := json.Unmarshal(payload, v); err != nil { return apperr.New("parse_payload", err, "解析任务载荷失败") } return nil }