From 58301bbed0b8a110da88f09b4586692d00e70d62 Mon Sep 17 00:00:00 2001 From: WuYuuuub <625806558@qq.com> Date: Wed, 17 Jun 2026 12:21:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86ai=E8=AF=86=E5=88=AB?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E7=A0=81=E9=83=A8=E5=88=86=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=B8=AA=E4=BA=BA=E6=88=90=E7=BB=A9=E8=A1=A5=E8=80=83?= =?UTF-8?q?=E6=A0=87=E8=AF=86=E5=92=8Ccet=E6=88=90=E7=BB=A9=E6=9F=A5?= =?UTF-8?q?=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- captcha/recognize.go | 56 +----- config/config.go | 54 +---- grpc/convert.go | 27 +++ grpc/handler.go | 2 + grpc/handler_helpers.go | 22 +-- parser/score.go | 41 +++- proto/runner/runner.pb.go | 350 +++++++++++++++++++++++++++++++-- proto/runner/runner.proto | 34 ++++ proto/runner/runner_grpc.pb.go | 4 +- 9 files changed, 456 insertions(+), 134 deletions(-) diff --git a/captcha/recognize.go b/captcha/recognize.go index f822167..74de1cf 100644 --- a/captcha/recognize.go +++ b/captcha/recognize.go @@ -1,55 +1,7 @@ package captcha -import ( - "fmt" - "io" - "log/slog" - "net/http" - - "schedule_converter/client" - "schedule_converter/config" -) - -func SaveCaptchaImage(httpClient *http.Client) ([]byte, error) { - req, err := http.NewRequest("GET", config.Get().BaseURL+"/captchaImage", nil) - if err != nil { - return nil, err - } - req.Header = client.GetHeaders() - - resp, err := httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("获取验证码失败, 状态码: %d", resp.StatusCode) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - return body, nil +// GetCaptchaCode 返回固定验证码 +// 教务系统验证码已不再需要识别,直接使用固定值 +func GetCaptchaCode() string { + return "...." } - -func GetAndRecognize(httpClient *http.Client) (string, error) { - _, err := SaveCaptchaImage(httpClient) - if err != nil { - return "", err - } - - captchaCode, err := Recognize("captcha.png") - if err != nil { - return "", err - } - - slog.Debug("验证码识别完成", "result", captchaCode) - return captchaCode, nil -} - -func Recognize(imagePath string) (string, error) { - return "....", nil -} \ No newline at end of file diff --git a/config/config.go b/config/config.go index 4b43557..a4710aa 100644 --- a/config/config.go +++ b/config/config.go @@ -1,27 +1,17 @@ package config import ( - "log/slog" "os" "strconv" "sync" - "time" ) type Config struct { - BaseURL string - Username string - Password string - Semester string - TWFID string - Recognizer RecognizerConfig -} - -type RecognizerConfig struct { - APIURL string - APIKey string - Model string - Timeout time.Duration + BaseURL string + Username string + Password string + Semester string + TWFID string } var ( @@ -37,16 +27,6 @@ func Load() *Config { Password: getEnv("JWTS_PASSWORD", "wyb12580963"), Semester: getEnv("JWTS_SEMESTER", "2025-20262"), TWFID: getEnv("JWTS_TWFID", ""), - Recognizer: RecognizerConfig{ - APIURL: getEnv("RECOGNIZER_API_URL", "https://api.littlelan.cn/v1/chat/completions"), - APIKey: getEnv("RECOGNIZER_API_KEY", "sk-6064466699a996b6f83e1ae8247c1cb4eeb4b72ce23cb001204335a2567d63c1"), - Model: getEnv("RECOGNIZER_MODEL", "qwen3.5-plus"), - Timeout: getEnvDuration("RECOGNIZER_TIMEOUT", 120*time.Second), - }, - } - - if cfg.Recognizer.APIKey == "" { - slog.Warn("RECOGNIZER_API_KEY not set, image recognition will not work") } }) return cfg @@ -78,24 +58,10 @@ func getEnvInt(key string, defaultValue int) int { return defaultValue } -func getEnvDuration(key string, defaultValue time.Duration) time.Duration { - val := os.Getenv(key) - if val == "" { - return defaultValue - } - if d, err := time.ParseDuration(val); err == nil { - return d - } - return defaultValue -} - var ( - BaseURL = Get().BaseURL - Username = Get().Username - Password = Get().Password - Semester = Get().Semester - TWFID = Get().TWFID - RecognizerAPIURL = Get().Recognizer.APIURL - RecognizerAPIKey = Get().Recognizer.APIKey - RecognizerModel = Get().Recognizer.Model + BaseURL = Get().BaseURL + Username = Get().Username + Password = Get().Password + Semester = Get().Semester + TWFID = Get().TWFID ) diff --git a/grpc/convert.go b/grpc/convert.go index 6697264..30490c8 100644 --- a/grpc/convert.go +++ b/grpc/convert.go @@ -49,6 +49,7 @@ func convertGrades(grades []parser.GradeEntry) []*pb.Grade { Score: g.Score, GradePoint: g.GradePoint, IsExam: g.IsExam, + IsRetake: g.IsRetake, }) } return result @@ -89,3 +90,29 @@ func convertEmptyClassrooms(entries []parser.EmptyClassroomEntry) []*pb.EmptyCla } 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 +} diff --git a/grpc/handler.go b/grpc/handler.go index 3daf44c..fc4050d 100644 --- a/grpc/handler.go +++ b/grpc/handler.go @@ -71,6 +71,8 @@ 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) } diff --git a/grpc/handler_helpers.go b/grpc/handler_helpers.go index adbc745..4cf37d7 100644 --- a/grpc/handler_helpers.go +++ b/grpc/handler_helpers.go @@ -26,7 +26,7 @@ const ( func createHTTPClient() (*http.Client, error) { jar, err := cookiejar.New(nil) if err != nil { - return nil, apperr.New("create_jar", err, "创建 cookie jar 失败") + return nil, apperr.New("create_jar", err, "鍒涘缓 cookie jar 澶辫触") } return &http.Client{Jar: jar}, nil } @@ -38,7 +38,7 @@ func 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) + slog.Warn("瑙f瀽 baseURL 澶辫触锛屾棤娉曡缃?TWFID cookie", "base_url", baseURL, "error", err) return } httpClient.Jar.SetCookies(u, []*http.Cookie{ @@ -50,7 +50,7 @@ func executeLogin(ctx context.Context, httpClient *http.Client, username, passwo log := slog.With("username", username) if err := client.GetInitialCookiesWithClient(httpClient); err != nil { - return apperr.New("get_cookies", err, "获取初始 Cookie 失败") + return apperr.New("get_cookies", err, "鑾峰彇鍒濆 Cookie 澶辫触") } for attempt := 1; attempt <= maxLoginRetries; attempt++ { @@ -60,18 +60,12 @@ func executeLogin(ctx context.Context, httpClient *http.Client, username, passwo 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 - } + 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) + log.Warn("鐧诲綍璇锋眰澶辫触", "error", err, "attempt", attempt) time.Sleep(loginRetryInterval) continue } @@ -80,7 +74,7 @@ func executeLogin(ctx context.Context, httpClient *http.Client, username, passwo return nil } - log.Warn("登录验证失败", "attempt", attempt) + log.Warn("鐧诲綍楠岃瘉澶辫触", "attempt", attempt) time.Sleep(loginRetryInterval) } @@ -102,7 +96,7 @@ func executeLoginAndFetchSchedule(ctx context.Context, httpClient *http.Client, func unmarshalPayload(payload []byte, v any) error { if err := json.Unmarshal(payload, v); err != nil { - return apperr.New("parse_payload", err, "解析任务载荷失败") + return apperr.New("parse_payload", err, "瑙f瀽浠诲姟杞借嵎澶辫触") } return nil } diff --git a/parser/score.go b/parser/score.go index 14f2868..cc9ef65 100644 --- a/parser/score.go +++ b/parser/score.go @@ -29,6 +29,7 @@ type GradeEntry struct { Score string GradePoint string IsExam string + IsRetake bool } type GPASummary struct { @@ -139,7 +140,7 @@ func parseGradesHTML(html string) ([]GradeEntry, error) { return } - texts := getCellTexts(cells) + texts, isRetake := getCellTextsAndRetake(cells) if len(texts) < 9 { return } @@ -147,10 +148,11 @@ func parseGradesHTML(html string) ([]GradeEntry, error) { isMidterm := len(texts) == 10 entry := GradeEntry{ - Term: safeGet(texts, 1), - Code: safeGet(texts, 3), - Name: safeGet(texts, 4), - Credit: safeGet(texts, 7), + Term: safeGet(texts, 1), + Code: safeGet(texts, 3), + Name: safeGet(texts, 4), + Credit: safeGet(texts, 7), + IsRetake: isRetake, } if isMidterm { @@ -227,7 +229,7 @@ func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) { return } - texts := getCellTexts(cells) + texts, isRetake := getCellTextsAndRetake(cells) entry := GradeEntry{ Term: safeGet(texts, 0), @@ -239,6 +241,7 @@ func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) { Credit: safeGet(texts, 7), Score: safeGet(texts, 8), GradePoint: safeGet(texts, 9), + IsRetake: isRetake, } if entry.Name != "" && len(entry.Name) >= 2 { @@ -250,6 +253,32 @@ func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) { return summary, grades, nil } +// getCellTextsAndRetake 解析单元格文本,同时检测是否包含补考标记 +// 补考标记在HTML中表现为 补考, +// 解析后该单元格文本为"补考",需要从texts中移除并记录isRetake标志 +func getCellTextsAndRetake(cells *goquery.Selection) ([]string, bool) { + var texts []string + isRetake := false + retakeIndex := -1 + + cells.Each(func(i int, cell *goquery.Selection) { + text := strings.TrimSpace(cell.Text()) + // 检测补考标记:补考 + if text == "补考" { + isRetake = true + retakeIndex = i + } + texts = append(texts, text) + }) + + // 移除"补考"这一列,使后续列索引与正常行一致 + if isRetake && retakeIndex >= 0 { + texts = append(texts[:retakeIndex], texts[retakeIndex+1:]...) + } + + return texts, isRetake +} + func getCellTexts(cells *goquery.Selection) []string { var texts []string cells.Each(func(_ int, cell *goquery.Selection) { diff --git a/proto/runner/runner.pb.go b/proto/runner/runner.pb.go index dff09e9..a39649c 100644 --- a/proto/runner/runner.pb.go +++ b/proto/runner/runner.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.11 -// protoc v7.34.1 +// protoc v6.32.0 // source: proto/runner/runner.proto package runner @@ -32,6 +32,7 @@ const ( TaskType_TASK_TYPE_GET_EXAMS TaskType = 4 // 获取考试安排 TaskType_TASK_TYPE_GET_USER_INFO TaskType = 5 // 获取用户信息 TaskType_TASK_TYPE_GET_EMPTY_CLASSROOM TaskType = 6 // 获取空教室 + TaskType_TASK_TYPE_GET_CET TaskType = 7 // 获取四六级成绩 ) // Enum value maps for TaskType. @@ -44,6 +45,7 @@ var ( 4: "TASK_TYPE_GET_EXAMS", 5: "TASK_TYPE_GET_USER_INFO", 6: "TASK_TYPE_GET_EMPTY_CLASSROOM", + 7: "TASK_TYPE_GET_CET", } TaskType_value = map[string]int32{ "TASK_TYPE_UNKNOWN": 0, @@ -53,6 +55,7 @@ var ( "TASK_TYPE_GET_EXAMS": 4, "TASK_TYPE_GET_USER_INFO": 5, "TASK_TYPE_GET_EMPTY_CLASSROOM": 6, + "TASK_TYPE_GET_CET": 7, } ) @@ -1378,6 +1381,7 @@ type Grade struct { Score string `protobuf:"bytes,7,opt,name=score,proto3" json:"score,omitempty"` // 成绩 (可能是等级或分数) GradePoint string `protobuf:"bytes,8,opt,name=grade_point,json=gradePoint,proto3" json:"grade_point,omitempty"` // 绩点 IsExam string `protobuf:"bytes,9,opt,name=is_exam,json=isExam,proto3" json:"is_exam,omitempty"` // 是否考试课 + IsRetake bool `protobuf:"varint,10,opt,name=is_retake,json=isRetake,proto3" json:"is_retake,omitempty"` // 是否为补考 unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1475,6 +1479,13 @@ func (x *Grade) GetIsExam() string { return "" } +func (x *Grade) GetIsRetake() bool { + if x != nil { + return x.IsRetake + } + return false +} + // ExamsResultData 考试安排结果数据 type ExamsResultData struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1945,6 +1956,282 @@ func (x *EmptyClassroomEntry) GetWeeks() string { return "" } +// GetCetPayload 获取四六级成绩任务载荷 +type GetCetPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` // 学号 + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` // 密码 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCetPayload) Reset() { + *x = GetCetPayload{} + mi := &file_proto_runner_runner_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCetPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCetPayload) ProtoMessage() {} + +func (x *GetCetPayload) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCetPayload.ProtoReflect.Descriptor instead. +func (*GetCetPayload) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{24} +} + +func (x *GetCetPayload) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *GetCetPayload) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +// CetScore 四六级成绩条目 +type CetScore struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level string `protobuf:"bytes,1,opt,name=level,proto3" json:"level,omitempty"` // 等级(英语四级/英语六级) + ExamTime string `protobuf:"bytes,2,opt,name=exam_time,json=examTime,proto3" json:"exam_time,omitempty"` // 考试时间,如 2025年06月 + TicketNum string `protobuf:"bytes,3,opt,name=ticket_num,json=ticketNum,proto3" json:"ticket_num,omitempty"` // 准考证号 + Score string `protobuf:"bytes,4,opt,name=score,proto3" json:"score,omitempty"` // 成绩 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CetScore) Reset() { + *x = CetScore{} + mi := &file_proto_runner_runner_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CetScore) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CetScore) ProtoMessage() {} + +func (x *CetScore) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CetScore.ProtoReflect.Descriptor instead. +func (*CetScore) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{25} +} + +func (x *CetScore) GetLevel() string { + if x != nil { + return x.Level + } + return "" +} + +func (x *CetScore) GetExamTime() string { + if x != nil { + return x.ExamTime + } + return "" +} + +func (x *CetScore) GetTicketNum() string { + if x != nil { + return x.TicketNum + } + return "" +} + +func (x *CetScore) GetScore() string { + if x != nil { + return x.Score + } + return "" +} + +// CetStudentInfo 四六级查询页面的学生信息 +type CetStudentInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + StudentId string `protobuf:"bytes,1,opt,name=student_id,json=studentId,proto3" json:"student_id,omitempty"` // 学号 + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` // 姓名 + Gender string `protobuf:"bytes,3,opt,name=gender,proto3" json:"gender,omitempty"` // 性别 + IdCard string `protobuf:"bytes,4,opt,name=id_card,json=idCard,proto3" json:"id_card,omitempty"` // 身份证号 + Duration string `protobuf:"bytes,5,opt,name=duration,proto3" json:"duration,omitempty"` // 学制 + Grade string `protobuf:"bytes,6,opt,name=grade,proto3" json:"grade,omitempty"` // 年级 + College string `protobuf:"bytes,7,opt,name=college,proto3" json:"college,omitempty"` // 学院 + Major string `protobuf:"bytes,8,opt,name=major,proto3" json:"major,omitempty"` // 专业 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CetStudentInfo) Reset() { + *x = CetStudentInfo{} + mi := &file_proto_runner_runner_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CetStudentInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CetStudentInfo) ProtoMessage() {} + +func (x *CetStudentInfo) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CetStudentInfo.ProtoReflect.Descriptor instead. +func (*CetStudentInfo) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{26} +} + +func (x *CetStudentInfo) GetStudentId() string { + if x != nil { + return x.StudentId + } + return "" +} + +func (x *CetStudentInfo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CetStudentInfo) GetGender() string { + if x != nil { + return x.Gender + } + return "" +} + +func (x *CetStudentInfo) GetIdCard() string { + if x != nil { + return x.IdCard + } + return "" +} + +func (x *CetStudentInfo) GetDuration() string { + if x != nil { + return x.Duration + } + return "" +} + +func (x *CetStudentInfo) GetGrade() string { + if x != nil { + return x.Grade + } + return "" +} + +func (x *CetStudentInfo) GetCollege() string { + if x != nil { + return x.College + } + return "" +} + +func (x *CetStudentInfo) GetMajor() string { + if x != nil { + return x.Major + } + return "" +} + +// CetResultData 四六级成绩结果数据 +type CetResultData struct { + state protoimpl.MessageState `protogen:"open.v1"` + StudentInfo *CetStudentInfo `protobuf:"bytes,1,opt,name=student_info,json=studentInfo,proto3" json:"student_info,omitempty"` // 学生信息 + Scores []*CetScore `protobuf:"bytes,2,rep,name=scores,proto3" json:"scores,omitempty"` // 成绩列表 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CetResultData) Reset() { + *x = CetResultData{} + mi := &file_proto_runner_runner_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CetResultData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CetResultData) ProtoMessage() {} + +func (x *CetResultData) ProtoReflect() protoreflect.Message { + mi := &file_proto_runner_runner_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CetResultData.ProtoReflect.Descriptor instead. +func (*CetResultData) Descriptor() ([]byte, []int) { + return file_proto_runner_runner_proto_rawDescGZIP(), []int{27} +} + +func (x *CetResultData) GetStudentInfo() *CetStudentInfo { + if x != nil { + return x.StudentInfo + } + return nil +} + +func (x *CetResultData) GetScores() []*CetScore { + if x != nil { + return x.Scores + } + return nil +} + var File_proto_runner_runner_proto protoreflect.FileDescriptor const file_proto_runner_runner_proto_rawDesc = "" + @@ -2047,7 +2334,7 @@ const file_proto_runner_runner_proto_rawDesc = "" + "student_id\x18\x02 \x01(\tR\tstudentId\x12%\n" + "\x06grades\x18\x03 \x03(\v2\r.runner.GradeR\x06grades\x123\n" + "\vgpa_summary\x18\x04 \x01(\v2\x12.runner.GpaSummaryR\n" + - "gpaSummary\"\xf9\x01\n" + + "gpaSummary\"\x96\x02\n" + "\x05Grade\x12\x12\n" + "\x04term\x18\x01 \x01(\tR\x04term\x12\x1f\n" + "\vcourse_code\x18\x02 \x01(\tR\n" + @@ -2060,7 +2347,9 @@ const file_proto_runner_runner_proto_rawDesc = "" + "\x05score\x18\a \x01(\tR\x05score\x12\x1f\n" + "\vgrade_point\x18\b \x01(\tR\n" + "gradePoint\x12\x17\n" + - "\ais_exam\x18\t \x01(\tR\x06isExam\"p\n" + + "\ais_exam\x18\t \x01(\tR\x06isExam\x12\x1b\n" + + "\tis_retake\x18\n" + + " \x01(\bR\bisRetake\"p\n" + "\x0fExamsResultData\x12\x1a\n" + "\bsemester\x18\x01 \x01(\tR\bsemester\x12\x1d\n" + "\n" + @@ -2106,7 +2395,29 @@ const file_proto_runner_runner_proto_rawDesc = "" + "\aweekday\x18\x02 \x01(\tR\aweekday\x12\x18\n" + "\aperiods\x18\x03 \x01(\tR\aperiods\x12\x12\n" + "\x04term\x18\x04 \x01(\tR\x04term\x12\x14\n" + - "\x05weeks\x18\x05 \x01(\tR\x05weeks*\xc5\x01\n" + + "\x05weeks\x18\x05 \x01(\tR\x05weeks\"G\n" + + "\rGetCetPayload\x12\x1a\n" + + "\busername\x18\x01 \x01(\tR\busername\x12\x1a\n" + + "\bpassword\x18\x02 \x01(\tR\bpassword\"r\n" + + "\bCetScore\x12\x14\n" + + "\x05level\x18\x01 \x01(\tR\x05level\x12\x1b\n" + + "\texam_time\x18\x02 \x01(\tR\bexamTime\x12\x1d\n" + + "\n" + + "ticket_num\x18\x03 \x01(\tR\tticketNum\x12\x14\n" + + "\x05score\x18\x04 \x01(\tR\x05score\"\xd6\x01\n" + + "\x0eCetStudentInfo\x12\x1d\n" + + "\n" + + "student_id\x18\x01 \x01(\tR\tstudentId\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + + "\x06gender\x18\x03 \x01(\tR\x06gender\x12\x17\n" + + "\aid_card\x18\x04 \x01(\tR\x06idCard\x12\x1a\n" + + "\bduration\x18\x05 \x01(\tR\bduration\x12\x14\n" + + "\x05grade\x18\x06 \x01(\tR\x05grade\x12\x18\n" + + "\acollege\x18\a \x01(\tR\acollege\x12\x14\n" + + "\x05major\x18\b \x01(\tR\x05major\"t\n" + + "\rCetResultData\x129\n" + + "\fstudent_info\x18\x01 \x01(\v2\x16.runner.CetStudentInfoR\vstudentInfo\x12(\n" + + "\x06scores\x18\x02 \x03(\v2\x10.runner.CetScoreR\x06scores*\xdc\x01\n" + "\bTaskType\x12\x15\n" + "\x11TASK_TYPE_UNKNOWN\x10\x00\x12\x13\n" + "\x0fTASK_TYPE_LOGIN\x10\x01\x12\x1a\n" + @@ -2114,7 +2425,8 @@ const file_proto_runner_runner_proto_rawDesc = "" + "\x14TASK_TYPE_GET_GRADES\x10\x03\x12\x17\n" + "\x13TASK_TYPE_GET_EXAMS\x10\x04\x12\x1b\n" + "\x17TASK_TYPE_GET_USER_INFO\x10\x05\x12!\n" + - "\x1dTASK_TYPE_GET_EMPTY_CLASSROOM\x10\x06*\x8a\x01\n" + + "\x1dTASK_TYPE_GET_EMPTY_CLASSROOM\x10\x06\x12\x15\n" + + "\x11TASK_TYPE_GET_CET\x10\a*\x8a\x01\n" + "\n" + "TaskStatus\x12\x17\n" + "\x13TASK_STATUS_UNKNOWN\x10\x00\x12\x17\n" + @@ -2138,7 +2450,7 @@ func file_proto_runner_runner_proto_rawDescGZIP() []byte { } var file_proto_runner_runner_proto_enumTypes = make([]protoimpl.EnumInfo, 2) -var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 25) +var file_proto_runner_runner_proto_msgTypes = make([]protoimpl.MessageInfo, 29) var file_proto_runner_runner_proto_goTypes = []any{ (TaskType)(0), // 0: runner.TaskType (TaskStatus)(0), // 1: runner.TaskStatus @@ -2166,7 +2478,11 @@ var file_proto_runner_runner_proto_goTypes = []any{ (*GetEmptyClassroomPayload)(nil), // 23: runner.GetEmptyClassroomPayload (*EmptyClassroomResultData)(nil), // 24: runner.EmptyClassroomResultData (*EmptyClassroomEntry)(nil), // 25: runner.EmptyClassroomEntry - nil, // 26: runner.RegisterRequest.CapabilitiesEntry + (*GetCetPayload)(nil), // 26: runner.GetCetPayload + (*CetScore)(nil), // 27: runner.CetScore + (*CetStudentInfo)(nil), // 28: runner.CetStudentInfo + (*CetResultData)(nil), // 29: runner.CetResultData + nil, // 30: runner.RegisterRequest.CapabilitiesEntry } var file_proto_runner_runner_proto_depIdxs = []int32{ 3, // 0: runner.StreamMessage.register:type_name -> runner.RegisterRequest @@ -2176,7 +2492,7 @@ var file_proto_runner_runner_proto_depIdxs = []int32{ 7, // 4: runner.StreamMessage.task:type_name -> runner.Task 9, // 5: runner.StreamMessage.result:type_name -> runner.TaskResult 8, // 6: runner.StreamMessage.task_cancel:type_name -> runner.TaskCancel - 26, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry + 30, // 7: runner.RegisterRequest.capabilities:type_name -> runner.RegisterRequest.CapabilitiesEntry 0, // 8: runner.Task.type:type_name -> runner.TaskType 1, // 9: runner.TaskResult.status:type_name -> runner.TaskStatus 15, // 10: runner.ScheduleResultData.courses:type_name -> runner.Course @@ -2185,13 +2501,15 @@ var file_proto_runner_runner_proto_depIdxs = []int32{ 17, // 13: runner.GradesResultData.gpa_summary:type_name -> runner.GpaSummary 21, // 14: runner.ExamsResultData.exams:type_name -> runner.Exam 25, // 15: runner.EmptyClassroomResultData.classrooms:type_name -> runner.EmptyClassroomEntry - 2, // 16: runner.RunnerHub.Connect:input_type -> runner.StreamMessage - 2, // 17: runner.RunnerHub.Connect:output_type -> runner.StreamMessage - 17, // [17:18] is the sub-list for method output_type - 16, // [16:17] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 28, // 16: runner.CetResultData.student_info:type_name -> runner.CetStudentInfo + 27, // 17: runner.CetResultData.scores:type_name -> runner.CetScore + 2, // 18: runner.RunnerHub.Connect:input_type -> runner.StreamMessage + 2, // 19: runner.RunnerHub.Connect:output_type -> runner.StreamMessage + 19, // [19:20] is the sub-list for method output_type + 18, // [18:19] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_proto_runner_runner_proto_init() } @@ -2214,7 +2532,7 @@ func file_proto_runner_runner_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_runner_runner_proto_rawDesc), len(file_proto_runner_runner_proto_rawDesc)), NumEnums: 2, - NumMessages: 25, + NumMessages: 29, NumExtensions: 0, NumServices: 1, }, diff --git a/proto/runner/runner.proto b/proto/runner/runner.proto index 7e51a55..1e517e7 100644 --- a/proto/runner/runner.proto +++ b/proto/runner/runner.proto @@ -82,6 +82,7 @@ enum TaskType { TASK_TYPE_GET_EXAMS = 4; // 获取考试安排 TASK_TYPE_GET_USER_INFO = 5; // 获取用户信息 TASK_TYPE_GET_EMPTY_CLASSROOM = 6; // 获取空教室 + TASK_TYPE_GET_CET = 7; // 获取四六级成绩 } // TaskStatus 任务状态枚举 @@ -208,6 +209,7 @@ message Grade { string score = 7; // 成绩 (可能是等级或分数) string grade_point = 8; // 绩点 string is_exam = 9; // 是否考试课 + bool is_retake = 10; // 是否为补考 } // ExamsResultData 考试安排结果数据 @@ -264,3 +266,35 @@ message EmptyClassroomEntry { string term = 4; // 学期描述 string weeks = 5; // 周次范围 } + +// GetCetPayload 获取四六级成绩任务载荷 +message GetCetPayload { + string username = 1; // 学号 + string password = 2; // 密码 +} + +// CetScore 四六级成绩条目 +message CetScore { + string level = 1; // 等级(英语四级/英语六级) + string exam_time = 2; // 考试时间,如 2025年06月 + string ticket_num = 3; // 准考证号 + string score = 4; // 成绩 +} + +// CetStudentInfo 四六级查询页面的学生信息 +message CetStudentInfo { + string student_id = 1; // 学号 + string name = 2; // 姓名 + string gender = 3; // 性别 + string id_card = 4; // 身份证号 + string duration = 5; // 学制 + string grade = 6; // 年级 + string college = 7; // 学院 + string major = 8; // 专业 +} + +// CetResultData 四六级成绩结果数据 +message CetResultData { + CetStudentInfo student_info = 1; // 学生信息 + repeated CetScore scores = 2; // 成绩列表 +} diff --git a/proto/runner/runner_grpc.pb.go b/proto/runner/runner_grpc.pb.go index 435ab41..0b089ad 100644 --- a/proto/runner/runner_grpc.pb.go +++ b/proto/runner/runner_grpc.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.6.2 -// - protoc v7.34.1 +// - protoc-gen-go-grpc v1.6.1 +// - protoc v6.32.0 // source: proto/runner/runner.proto package runner