All checks were successful
Build / build (push) Successful in 2m17s
Extract business logic from gRPC handlers into a dedicated service package. Add context support throughout for cancellation and timeouts. Move models to their own package, remove hardcoded credentials from config, and simplify parsers to only handle HTML parsing.
102 lines
2.5 KiB
Go
102 lines
2.5 KiB
Go
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
|
|
}
|