refactor(grpc): restructure task handling and migrate to slog
All checks were successful
Build / build (push) Successful in 5m28s

Refactor the gRPC client and task handling logic to improve modularity,
error handling, and observability.

- Split `grpc/handler.go` into specialized handler files (login,
  classroom, exams, grades, schedule) to reduce complexity.
- Replace standard `log` with `log/slog` throughout the project for
  structured logging.
- Refactor `grpc/client.go` to use a thread-safe connection management
  pattern with `sync.RWMutex`.
- Remove the legacy `service` package and HTTP server implementation,
  transitioning the application to a pure gRPC runner mode.
- Clean up `config` and `pkg/errors` to remove unused utilities and
  simplify the API.
- Improve error reporting in parsers and HTTP clients by checking
  response status codes.
This commit is contained in:
2026-06-09 23:08:03 +08:00
parent ee5c36f3ac
commit accc6567f2
26 changed files with 720 additions and 1442 deletions

View File

@@ -1,124 +0,0 @@
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)
}