Files
schedule_converter/parser/score.go
lan 478ec3e3ab
All checks were successful
Build / build (push) Successful in 2m14s
feat(grpc): add support for fetching grades and exams
Implement new gRPC task handlers for retrieving student grades and exam schedules. This includes adding new parser logic for processing score and exam data, and updating the client capabilities to advertise support for these new task types.

- Add `getGrades` and `getExams` handlers in `grpc/handler.go`
- Add `parser/encoding.go`, `parser/exam.go`, and `parser/score.go` for data parsing
- Update `grpc/client.go` to include `TASK_TYPE_GET_GRADES` and `TASK_TYPE_GET_EXAMS` in capabilities
2026-05-11 00:46:38 +08:00

257 lines
5.2 KiB
Go

package parser
import (
"fmt"
"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.BaseURL + "/cjcx/queryQzcj"
default:
gradeURL = config.BaseURL + "/cjcx/queryQmcj"
}
fmt.Printf("[成绩] 正在获取成绩数据 (%s)...\n", ExamType)
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()
html, err := decodeResponseBody(resp)
if err != nil {
return nil, err
}
grades, err := ParseGradesHTML(html)
if err != nil {
return nil, err
}
fmt.Printf("[成绩] 共获取 %d 条成绩记录\n", len(grades))
return grades, nil
}
func FetchGPA(httpClient *http.Client) (*GPASummary, []GradeEntry, error) {
gpaURL := config.BaseURL + "/xfj/queryListXfj"
fmt.Println("[学分绩] 正在获取学分绩数据...")
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()
html, err := decodeResponseBody(resp)
if err != nil {
return nil, nil, err
}
summary, grades, err := ParseGPAHTML(html)
if err != nil {
return nil, nil, err
}
fmt.Printf("[学分绩] 共获取 %d 门课程明细\n", 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 ""
}