125 lines
3.1 KiB
Go
125 lines
3.1 KiB
Go
|
|
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)
|
||
|
|
}
|