package parser import ( "fmt" "log/slog" "net/http" "strings" "schedule_converter/client" "schedule_converter/config" "github.com/PuerkitoBio/goquery" ) type ScoreType string const ( ScoreTypeFinal ScoreType = "final" ScoreTypeMidterm ScoreType = "midterm" ) type GradeEntry struct { Term string Code string Name string Nature string Category string Credit string Score string GradePoint string IsExam string } type GPASummary struct { AverageGPA string Rank string ExamTotalGPA string ExamTotalCredits string FailCredits string Semesters string } func FetchGrades(httpClient *http.Client, examType ScoreType) ([]GradeEntry, error) { var gradeURL string switch examType { case ScoreTypeMidterm: gradeURL = config.Get().BaseURL + "/cjcx/queryQzcj" default: gradeURL = config.Get().BaseURL + "/cjcx/queryQmcj" } log := slog.With("exam_type", string(examType)) log.Info("获取成绩数据") req, err := http.NewRequest("GET", gradeURL, 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) } html, err := decodeResponseBody(resp) if err != nil { return nil, err } grades, err := parseGradesHTML(html) if err != nil { return nil, err } log.Info("成绩获取完成", "grade_count", len(grades)) return grades, nil } func FetchGPA(httpClient *http.Client) (*GPASummary, []GradeEntry, error) { gpaURL := config.Get().BaseURL + "/xfj/queryListXfj" slog.Info("获取学分绩数据") req, err := http.NewRequest("GET", gpaURL, nil) if err != nil { return nil, nil, err } req.Header = client.GetHeaders() resp, err := httpClient.Do(req) if err != nil { return nil, nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, nil, fmt.Errorf("获取学分绩失败, 状态码: %d", resp.StatusCode) } html, err := decodeResponseBody(resp) if err != nil { return nil, nil, err } summary, grades, err := parseGPAHTML(html) if err != nil { return nil, nil, err } slog.Info("学分绩获取完成", "grade_count", len(grades)) return summary, grades, nil } func parseGradesHTML(html string) ([]GradeEntry, error) { doc, err := goquery.NewDocumentFromReader(strings.NewReader(html)) if err != nil { return nil, err } var grades []GradeEntry doc.Find("table.bot_line").Each(func(_ int, table *goquery.Selection) { rows := table.Find("tr") if rows.Length() < 2 { return } rows.Each(func(idx int, row *goquery.Selection) { if idx == 0 { return } cells := row.Find("td") if cells.Length() < 8 { return } texts := getCellTexts(cells) if len(texts) < 9 { return } isMidterm := len(texts) == 10 entry := GradeEntry{ Term: safeGet(texts, 1), Code: safeGet(texts, 3), Name: safeGet(texts, 4), Credit: safeGet(texts, 7), } if isMidterm { entry.Score = safeGet(texts, 8) if len(texts) > 9 { entry.GradePoint = safeGet(texts, 9) } } else { if len(texts) >= 13 { entry.Nature = safeGet(texts, 3) entry.Category = safeGet(texts, 4) entry.Score = safeGet(texts, 12) entry.GradePoint = safeGet(texts, 9) entry.IsExam = safeGet(texts, 6) } else if len(texts) >= 10 { entry.Score = safeGet(texts, 8) entry.GradePoint = safeGet(texts, 9) } } if entry.Name != "" && len(entry.Name) >= 2 { if entry.Score == "" { entry.Score = "-" } grades = append(grades, entry) } }) }) return grades, nil } func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) { doc, err := goquery.NewDocumentFromReader(strings.NewReader(html)) if err != nil { return nil, nil, err } summary := &GPASummary{} doc.Find("span").Each(func(_ int, span *goquery.Selection) { id, _ := span.Attr("id") text := strings.TrimSpace(span.Text()) switch id { case "pjxfj": summary.AverageGPA = text case "zrs": summary.Rank = text case "kskxfj": summary.ExamTotalGPA = text case "kskxfs": summary.ExamTotalCredits = text case "kckxfj": summary.FailCredits = text case "xqs": summary.Semesters = text } }) var grades []GradeEntry doc.Find("table.bot_line").Each(func(_ int, table *goquery.Selection) { rows := table.Find("tr") if rows.Length() < 2 { return } rows.Each(func(idx int, row *goquery.Selection) { if idx == 0 { return } cells := row.Find("td") if cells.Length() < 10 { return } texts := getCellTexts(cells) entry := GradeEntry{ Term: safeGet(texts, 0), Code: safeGet(texts, 1), Name: safeGet(texts, 2), Nature: safeGet(texts, 3), Category: safeGet(texts, 4), IsExam: safeGet(texts, 6), Credit: safeGet(texts, 7), Score: safeGet(texts, 8), GradePoint: safeGet(texts, 9), } if entry.Name != "" && len(entry.Name) >= 2 { grades = append(grades, entry) } }) }) return summary, grades, nil } func getCellTexts(cells *goquery.Selection) []string { var texts []string cells.Each(func(_ int, cell *goquery.Selection) { text := strings.TrimSpace(cell.Text()) texts = append(texts, text) }) return texts } func safeGet(arr []string, idx int) string { if idx < len(arr) { return arr[idx] } return "" }