feat(grpc): add support for fetching grades and exams
All checks were successful
Build / build (push) Successful in 2m14s
All checks were successful
Build / build (push) Successful in 2m14s
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
This commit is contained in:
257
parser/score.go
Normal file
257
parser/score.go
Normal file
@@ -0,0 +1,257 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user