refactor: introduce service layer and migrate gRPC handlers to use it
All checks were successful
Build / build (push) Successful in 2m17s
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.
This commit is contained in:
@@ -78,18 +78,16 @@ func (c *Client) Connect(ctx context.Context) error {
|
||||
|
||||
// register 发送注册请求
|
||||
func (c *Client) register() error {
|
||||
capabilities := make(map[string]string, len(supportedTaskTypes()))
|
||||
for _, t := range supportedTaskTypes() {
|
||||
capabilities[t.String()] = "true"
|
||||
}
|
||||
msg := &pb.StreamMessage{
|
||||
Message: &pb.StreamMessage_Register{
|
||||
Register: &pb.RegisterRequest{
|
||||
RunnerId: c.runnerID,
|
||||
Version: c.version,
|
||||
Capabilities: map[string]string{
|
||||
"TASK_TYPE_GET_SCHEDULE": "true",
|
||||
"TASK_TYPE_GET_GRADES": "true",
|
||||
"TASK_TYPE_GET_EXAMS": "true",
|
||||
"TASK_TYPE_LOGIN": "true",
|
||||
"TASK_TYPE_GET_EMPTY_CLASSROOM": "true",
|
||||
},
|
||||
RunnerId: c.runnerID,
|
||||
Version: c.version,
|
||||
Capabilities: capabilities,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
pb "schedule_converter/proto/runner"
|
||||
|
||||
"schedule_converter/models"
|
||||
"schedule_converter/parser"
|
||||
)
|
||||
|
||||
func convertCourses(courses []models.Course) []*pb.Course {
|
||||
result := make([]*pb.Course, len(courses))
|
||||
for i, c := range courses {
|
||||
schedule := make([]*pb.ScheduleTime, len(c.Schedule))
|
||||
for j, s := range c.Schedule {
|
||||
schedule[j] = &pb.ScheduleTime{
|
||||
Day: s.Day,
|
||||
Sections: convertToInt32(s.Section),
|
||||
Weeks: convertToInt32(s.Weeks),
|
||||
}
|
||||
}
|
||||
result[i] = &pb.Course{
|
||||
Name: c.Name,
|
||||
Teacher: c.Teacher,
|
||||
Room: c.Room,
|
||||
Schedule: schedule,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertToInt32(arr []int) []int32 {
|
||||
result := make([]int32, len(arr))
|
||||
for i, v := range arr {
|
||||
result[i] = int32(v)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertGrades(grades []parser.GradeEntry) []*pb.Grade {
|
||||
result := make([]*pb.Grade, 0, len(grades))
|
||||
for _, g := range grades {
|
||||
result = append(result, &pb.Grade{
|
||||
Term: g.Term,
|
||||
CourseCode: g.Code,
|
||||
CourseName: g.Name,
|
||||
Nature: g.Nature,
|
||||
Category: g.Category,
|
||||
Credit: g.Credit,
|
||||
Score: g.Score,
|
||||
GradePoint: g.GradePoint,
|
||||
IsExam: g.IsExam,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertExams(exams []parser.ExamEntry) []*pb.Exam {
|
||||
result := make([]*pb.Exam, 0, len(exams))
|
||||
for _, e := range exams {
|
||||
examTypeStr := "期末"
|
||||
switch e.Type {
|
||||
case parser.ExamTypeMidterm:
|
||||
examTypeStr = "期中"
|
||||
case parser.ExamTypeMakeup:
|
||||
examTypeStr = "补考"
|
||||
}
|
||||
result = append(result, &pb.Exam{
|
||||
CourseName: e.Course,
|
||||
ExamType: examTypeStr,
|
||||
Date: e.Date,
|
||||
Time: e.Time,
|
||||
Week: e.Week,
|
||||
Weekday: e.Weekday,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertEmptyClassrooms(entries []parser.EmptyClassroomEntry) []*pb.EmptyClassroomEntry {
|
||||
result := make([]*pb.EmptyClassroomEntry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
result = append(result, &pb.EmptyClassroomEntry{
|
||||
Classroom: e.Classroom,
|
||||
Weekday: e.Weekday,
|
||||
Periods: e.Periods,
|
||||
Term: e.Term,
|
||||
Weeks: e.Weeks,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -2,21 +2,28 @@ package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"schedule_converter/service"
|
||||
|
||||
pb "schedule_converter/proto/runner"
|
||||
)
|
||||
|
||||
type TaskHandler struct {
|
||||
cancelledTasks sync.Map
|
||||
service *service.Service
|
||||
cancels sync.Map // taskID -> context.CancelFunc(运行中的任务)
|
||||
preCancelled sync.Map // taskID -> struct{}(任务开始前就已取消)
|
||||
}
|
||||
|
||||
func NewTaskHandler() *TaskHandler {
|
||||
return &TaskHandler{}
|
||||
return &TaskHandler{
|
||||
service: service.NewService(),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult {
|
||||
@@ -28,14 +35,17 @@ func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult {
|
||||
CompletedAt: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
if h.IsCancelled(task.TaskId) {
|
||||
// 任务开始前检查是否已被预取消。
|
||||
if _, cancelled := h.preCancelled.LoadAndDelete(task.TaskId); cancelled {
|
||||
result.Status = pb.TaskStatus_TASK_STATUS_CANCELLED
|
||||
result.ErrorMessage = "任务已取消"
|
||||
log.Info("任务已取消")
|
||||
return result
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx, cancel := h.buildTaskContext(task)
|
||||
defer cancel()
|
||||
|
||||
data, err := h.dispatch(ctx, task)
|
||||
|
||||
if err != nil {
|
||||
@@ -57,31 +67,68 @@ func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult {
|
||||
return result
|
||||
}
|
||||
|
||||
var errUnknownTaskType = fmt.Errorf("unknown task type")
|
||||
|
||||
func (h *TaskHandler) dispatch(ctx context.Context, task *pb.Task) ([]byte, error) {
|
||||
switch task.Type {
|
||||
case pb.TaskType_TASK_TYPE_GET_SCHEDULE:
|
||||
return h.getSchedule(ctx, task.Payload)
|
||||
case pb.TaskType_TASK_TYPE_GET_GRADES:
|
||||
return h.getGrades(ctx, task.Payload)
|
||||
case pb.TaskType_TASK_TYPE_GET_EXAMS:
|
||||
return h.getExams(ctx, task.Payload)
|
||||
case pb.TaskType_TASK_TYPE_LOGIN:
|
||||
return h.login(ctx, task.Payload)
|
||||
case pb.TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM:
|
||||
return h.getEmptyClassrooms(ctx, task.Payload)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %v", errUnknownTaskType, task.Type)
|
||||
// buildTaskContext 构造任务执行 context:根据 TimeoutSeconds 设置截止时间,
|
||||
// 并注册 cancel 函数,使 CancelTask 能够中断正在执行的任务。
|
||||
func (h *TaskHandler) buildTaskContext(task *pb.Task) (context.Context, context.CancelFunc) {
|
||||
ctx := context.Background()
|
||||
if task.TimeoutSeconds > 0 {
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(task.TimeoutSeconds)*time.Second)
|
||||
h.cancels.Store(task.TaskId, context.CancelFunc(cancel))
|
||||
return ctx, func() {
|
||||
h.cancels.Delete(task.TaskId)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
h.cancels.Store(task.TaskId, context.CancelFunc(cancel))
|
||||
return ctx, func() {
|
||||
h.cancels.Delete(task.TaskId)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
var errUnknownTaskType = fmt.Errorf("unknown task type")
|
||||
|
||||
// taskHandlers 将任务类型映射到处理方法。
|
||||
var taskHandlers = map[pb.TaskType]func(h *TaskHandler, ctx context.Context, payload []byte) ([]byte, error){
|
||||
pb.TaskType_TASK_TYPE_GET_SCHEDULE: (*TaskHandler).getSchedule,
|
||||
pb.TaskType_TASK_TYPE_GET_GRADES: (*TaskHandler).getGrades,
|
||||
pb.TaskType_TASK_TYPE_GET_EXAMS: (*TaskHandler).getExams,
|
||||
pb.TaskType_TASK_TYPE_LOGIN: (*TaskHandler).login,
|
||||
pb.TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM: (*TaskHandler).getEmptyClassrooms,
|
||||
}
|
||||
|
||||
// supportedTaskTypes 返回当前支持的任务类型列表,供注册 capabilities 使用。
|
||||
func supportedTaskTypes() []pb.TaskType {
|
||||
types := make([]pb.TaskType, 0, len(taskHandlers))
|
||||
for t := range taskHandlers {
|
||||
types = append(types, t)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
func (h *TaskHandler) dispatch(ctx context.Context, task *pb.Task) ([]byte, error) {
|
||||
handler, ok := taskHandlers[task.Type]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %v", errUnknownTaskType, task.Type)
|
||||
}
|
||||
return handler(h, ctx, task.Payload)
|
||||
}
|
||||
|
||||
// CancelTask 取消任务:若任务正在执行则中断其 context;若尚未开始则记为预取消。
|
||||
func (h *TaskHandler) CancelTask(taskID string) {
|
||||
h.cancelledTasks.Store(taskID, true)
|
||||
if v, ok := h.cancels.LoadAndDelete(taskID); ok {
|
||||
if cancel, ok := v.(context.CancelFunc); ok {
|
||||
cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
// 任务尚未开始执行,标记为预取消,待 Execute 时直接返回取消。
|
||||
h.preCancelled.Store(taskID, struct{}{})
|
||||
slog.Info("任务已标记为取消", "task_id", taskID)
|
||||
}
|
||||
|
||||
func (h *TaskHandler) IsCancelled(taskID string) bool {
|
||||
v, ok := h.cancelledTasks.Load(taskID)
|
||||
return ok && v.(bool)
|
||||
// unmarshalPayload 解码任务载荷。
|
||||
func unmarshalPayload(payload []byte, v any) error {
|
||||
return json.Unmarshal(payload, v)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
|
||||
pb "schedule_converter/proto/runner"
|
||||
|
||||
"schedule_converter/config"
|
||||
"schedule_converter/parser"
|
||||
apperr "schedule_converter/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -21,31 +19,11 @@ func (h *TaskHandler) getEmptyClassrooms(ctx context.Context, payload []byte) ([
|
||||
log := slog.With("username", req.Username, "semester", req.Semester)
|
||||
log.Info("获取空教室任务")
|
||||
|
||||
httpClient, err := createHTTPClient()
|
||||
result, err := h.service.GetEmptyClassrooms(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperr.New("get_empty_classrooms", err, "获取空教室失败")
|
||||
}
|
||||
|
||||
cfg := config.Get()
|
||||
setTWFIDCookie(httpClient, cfg.BaseURL, "")
|
||||
|
||||
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
classrooms, err := parser.FetchEmptyClassrooms(httpClient, req.Semester, req.WeekStart, req.WeekEnd, req.CampusCode)
|
||||
if err != nil {
|
||||
return nil, apperr.New("fetch_empty_classrooms", err, "获取空教室失败")
|
||||
}
|
||||
|
||||
pbClassrooms := convertEmptyClassrooms(classrooms)
|
||||
|
||||
result := &pb.EmptyClassroomResultData{
|
||||
StudentId: req.Username,
|
||||
Semester: req.Semester,
|
||||
Classrooms: pbClassrooms,
|
||||
}
|
||||
|
||||
log.Info("空教室获取成功", "classroom_count", len(pbClassrooms))
|
||||
log.Info("空教室获取成功", "classroom_count", len(result.Classrooms))
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
|
||||
pb "schedule_converter/proto/runner"
|
||||
|
||||
"schedule_converter/config"
|
||||
"schedule_converter/parser"
|
||||
apperr "schedule_converter/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -21,31 +19,11 @@ func (h *TaskHandler) getExams(ctx context.Context, payload []byte) ([]byte, err
|
||||
log := slog.With("username", req.Username, "semester", req.Semester)
|
||||
log.Info("获取考试安排任务")
|
||||
|
||||
httpClient, err := createHTTPClient()
|
||||
result, err := h.service.GetExams(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperr.New("get_exams", err, "获取考试安排失败")
|
||||
}
|
||||
|
||||
cfg := config.Get()
|
||||
setTWFIDCookie(httpClient, cfg.BaseURL, "")
|
||||
|
||||
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
exams, err := parser.FetchAllExams(httpClient)
|
||||
if err != nil {
|
||||
return nil, apperr.New("fetch_exams", err, "获取考试安排失败")
|
||||
}
|
||||
|
||||
pbExams := convertExams(exams)
|
||||
|
||||
result := &pb.ExamsResultData{
|
||||
StudentId: req.Username,
|
||||
Semester: req.Semester,
|
||||
Exams: pbExams,
|
||||
}
|
||||
|
||||
log.Info("考试安排获取成功", "exam_count", len(pbExams))
|
||||
log.Info("考试安排获取成功", "exam_count", len(result.Exams))
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
|
||||
pb "schedule_converter/proto/runner"
|
||||
|
||||
"schedule_converter/config"
|
||||
"schedule_converter/parser"
|
||||
apperr "schedule_converter/pkg/errors"
|
||||
)
|
||||
|
||||
@@ -21,46 +19,11 @@ func (h *TaskHandler) getGrades(ctx context.Context, payload []byte) ([]byte, er
|
||||
log := slog.With("username", req.Username, "semester", req.Semester)
|
||||
log.Info("获取成绩任务")
|
||||
|
||||
httpClient, err := createHTTPClient()
|
||||
result, err := h.service.GetGrades(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperr.New("get_grades", err, "获取成绩失败")
|
||||
}
|
||||
|
||||
cfg := config.Get()
|
||||
setTWFIDCookie(httpClient, cfg.BaseURL, "")
|
||||
|
||||
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var gpaSummary *pb.GpaSummary
|
||||
summary, grades, err := parser.FetchGPA(httpClient)
|
||||
if err != nil {
|
||||
log.Warn("获取GPA页面失败,尝试成绩页面", "error", err)
|
||||
grades, err = parser.FetchGrades(httpClient, parser.ScoreTypeFinal)
|
||||
if err != nil {
|
||||
return nil, apperr.New("fetch_grades", err, "获取成绩失败")
|
||||
}
|
||||
} else if summary != nil {
|
||||
gpaSummary = &pb.GpaSummary{
|
||||
AverageGpa: summary.AverageGPA,
|
||||
Rank: summary.Rank,
|
||||
ExamTotalGpa: summary.ExamTotalGPA,
|
||||
ExamTotalCredits: summary.ExamTotalCredits,
|
||||
FailCredits: summary.FailCredits,
|
||||
Semesters: summary.Semesters,
|
||||
}
|
||||
}
|
||||
|
||||
pbGrades := convertGrades(grades)
|
||||
|
||||
result := &pb.GradesResultData{
|
||||
StudentId: req.Username,
|
||||
Semester: req.Semester,
|
||||
Grades: pbGrades,
|
||||
GpaSummary: gpaSummary,
|
||||
}
|
||||
|
||||
log.Info("成绩获取成功", "grade_count", len(pbGrades))
|
||||
log.Info("成绩获取成功", "grade_count", len(result.Grades))
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
pb "schedule_converter/proto/runner"
|
||||
|
||||
"schedule_converter/config"
|
||||
apperr "schedule_converter/pkg/errors"
|
||||
)
|
||||
|
||||
func (h *TaskHandler) login(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
@@ -19,16 +19,8 @@ func (h *TaskHandler) login(ctx context.Context, payload []byte) ([]byte, error)
|
||||
log := slog.With("username", req.Username)
|
||||
log.Info("登录任务")
|
||||
|
||||
httpClient, err := createHTTPClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := config.Get()
|
||||
setTWFIDCookie(httpClient, cfg.BaseURL, req.Twfid)
|
||||
|
||||
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
|
||||
return nil, err
|
||||
if _, err := h.service.Login(ctx, req.Username, req.Password, req.Twfid); err != nil {
|
||||
return nil, apperr.New("login", err, "登录失败")
|
||||
}
|
||||
|
||||
log.Info("登录成功")
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
pb "schedule_converter/proto/runner"
|
||||
|
||||
"schedule_converter/config"
|
||||
apperr "schedule_converter/pkg/errors"
|
||||
)
|
||||
|
||||
func (h *TaskHandler) getSchedule(ctx context.Context, payload []byte) ([]byte, error) {
|
||||
@@ -19,25 +19,11 @@ func (h *TaskHandler) getSchedule(ctx context.Context, payload []byte) ([]byte,
|
||||
log := slog.With("username", req.Username, "semester", req.Semester)
|
||||
log.Info("获取课表任务")
|
||||
|
||||
httpClient, err := createHTTPClient()
|
||||
result, err := h.service.GetSchedule(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, apperr.New("get_schedule", err, "获取课表失败")
|
||||
}
|
||||
|
||||
cfg := config.Get()
|
||||
setTWFIDCookie(httpClient, cfg.BaseURL, "")
|
||||
|
||||
courses, err := executeLoginAndFetchSchedule(ctx, httpClient, req.Username, req.Password, req.Semester)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &pb.ScheduleResultData{
|
||||
StudentId: req.Username,
|
||||
Semester: req.Semester,
|
||||
Courses: convertCourses(courses),
|
||||
}
|
||||
|
||||
log.Info("课表获取成功", "course_count", len(courses))
|
||||
log.Info("课表获取成功", "course_count", len(result.Courses))
|
||||
return json.Marshal(result)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user