2026-05-11 00:46:38 +08:00
|
|
|
package parser
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
2026-06-09 23:08:03 +08:00
|
|
|
"log/slog"
|
2026-05-11 00:46:38 +08:00
|
|
|
"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
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
func FetchGrades(httpClient *http.Client, examType ScoreType) ([]GradeEntry, error) {
|
2026-05-11 00:46:38 +08:00
|
|
|
var gradeURL string
|
2026-06-09 23:08:03 +08:00
|
|
|
switch examType {
|
2026-05-11 00:46:38 +08:00
|
|
|
case ScoreTypeMidterm:
|
2026-06-09 23:08:03 +08:00
|
|
|
gradeURL = config.Get().BaseURL + "/cjcx/queryQzcj"
|
2026-05-11 00:46:38 +08:00
|
|
|
default:
|
2026-06-09 23:08:03 +08:00
|
|
|
gradeURL = config.Get().BaseURL + "/cjcx/queryQmcj"
|
2026-05-11 00:46:38 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
log := slog.With("exam_type", string(examType))
|
|
|
|
|
log.Info("获取成绩数据")
|
2026-05-11 00:46:38 +08:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return nil, fmt.Errorf("获取成绩失败, 状态码: %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:46:38 +08:00
|
|
|
html, err := decodeResponseBody(resp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
grades, err := parseGradesHTML(html)
|
2026-05-11 00:46:38 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
log.Info("成绩获取完成", "grade_count", len(grades))
|
2026-05-11 00:46:38 +08:00
|
|
|
return grades, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func FetchGPA(httpClient *http.Client) (*GPASummary, []GradeEntry, error) {
|
2026-06-09 23:08:03 +08:00
|
|
|
gpaURL := config.Get().BaseURL + "/xfj/queryListXfj"
|
|
|
|
|
slog.Info("获取学分绩数据")
|
2026-05-11 00:46:38 +08:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return nil, nil, fmt.Errorf("获取学分绩失败, 状态码: %d", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:46:38 +08:00
|
|
|
html, err := decodeResponseBody(resp)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
summary, grades, err := parseGPAHTML(html)
|
2026-05-11 00:46:38 +08:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
slog.Info("学分绩获取完成", "grade_count", len(grades))
|
2026-05-11 00:46:38 +08:00
|
|
|
return summary, grades, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
func parseGradesHTML(html string) ([]GradeEntry, error) {
|
2026-05-11 00:46:38 +08:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 23:08:03 +08:00
|
|
|
func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) {
|
2026-05-11 00:46:38 +08:00
|
|
|
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 ""
|
2026-06-09 23:08:03 +08:00
|
|
|
}
|