Merge remote-tracking branch 'origin/master'
# Conflicts: # config/config.go # grpc/convert.go # grpc/handler.go # parser/score.go # service/service.go
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
118
grpc/convert.go
118
grpc/convert.go
@@ -1,118 +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,
|
||||
IsRetake: g.IsRetake,
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func convertCETStudentInfo(info parser.CETStudentInfo) *pb.CetStudentInfo {
|
||||
return &pb.CetStudentInfo{
|
||||
StudentId: info.StudentID,
|
||||
Name: info.Name,
|
||||
Gender: info.Gender,
|
||||
IdCard: info.IDCard,
|
||||
Duration: info.Duration,
|
||||
Grade: info.Grade,
|
||||
College: info.College,
|
||||
Major: info.Major,
|
||||
}
|
||||
}
|
||||
|
||||
func convertCETScores(scores []parser.CETScore) []*pb.CetScore {
|
||||
result := make([]*pb.CetScore, 0, len(scores))
|
||||
for _, s := range scores {
|
||||
result = append(result, &pb.CetScore{
|
||||
Level: s.Level,
|
||||
ExamTime: s.ExamTime,
|
||||
TicketNum: s.TicketNum,
|
||||
Score: s.Score,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -71,8 +71,6 @@ func (h *TaskHandler) dispatch(ctx context.Context, task *pb.Task) ([]byte, erro
|
||||
return h.login(ctx, task.Payload)
|
||||
case pb.TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM:
|
||||
return h.getEmptyClassrooms(ctx, task.Payload)
|
||||
case pb.TaskType_TASK_TYPE_GET_CET:
|
||||
return h.getCET(ctx, task.Payload)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %v", errUnknownTaskType, task.Type)
|
||||
}
|
||||
|
||||
@@ -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,102 +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("瑙f瀽 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 := captcha.GetCaptchaCode()
|
||||
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, "瑙f瀽浠诲姟杞借嵎澶辫触")
|
||||
}
|
||||
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