refactor: introduce service layer and migrate gRPC handlers to use it
All checks were successful
Build / build (push) Successful in 2m17s
All checks were successful
Build / build (push) Successful in 2m17s
Extract business logic from gRPC handlers into a dedicated service package. Add context support throughout for cancellation and timeouts. Move models to their own package, remove hardcoded credentials from config, and simplify parsers to only handle HTML parsing.
This commit is contained in:
126
parser/score.go
126
parser/score.go
@@ -1,128 +1,21 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"schedule_converter/client"
|
||||
"schedule_converter/config"
|
||||
"schedule_converter/models"
|
||||
|
||||
"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) {
|
||||
// ParseGradesHTML 解析成绩页面 HTML,返回成绩条目列表。
|
||||
func ParseGradesHTML(html string) ([]models.GradeEntry, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var grades []GradeEntry
|
||||
var grades []models.GradeEntry
|
||||
|
||||
doc.Find("table.bot_line").Each(func(_ int, table *goquery.Selection) {
|
||||
rows := table.Find("tr")
|
||||
@@ -146,7 +39,7 @@ func parseGradesHTML(html string) ([]GradeEntry, error) {
|
||||
|
||||
isMidterm := len(texts) == 10
|
||||
|
||||
entry := GradeEntry{
|
||||
entry := models.GradeEntry{
|
||||
Term: safeGet(texts, 1),
|
||||
Code: safeGet(texts, 3),
|
||||
Name: safeGet(texts, 4),
|
||||
@@ -183,13 +76,14 @@ func parseGradesHTML(html string) ([]GradeEntry, error) {
|
||||
return grades, nil
|
||||
}
|
||||
|
||||
func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) {
|
||||
// ParseGPAHTML 解析学分绩页面 HTML,返回 GPA 概要与成绩条目列表。
|
||||
func ParseGPAHTML(html string) (*models.GPASummary, []models.GradeEntry, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
summary := &GPASummary{}
|
||||
summary := &models.GPASummary{}
|
||||
|
||||
doc.Find("span").Each(func(_ int, span *goquery.Selection) {
|
||||
id, _ := span.Attr("id")
|
||||
@@ -210,7 +104,7 @@ func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) {
|
||||
}
|
||||
})
|
||||
|
||||
var grades []GradeEntry
|
||||
var grades []models.GradeEntry
|
||||
|
||||
doc.Find("table.bot_line").Each(func(_ int, table *goquery.Selection) {
|
||||
rows := table.Find("tr")
|
||||
@@ -229,7 +123,7 @@ func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) {
|
||||
|
||||
texts := getCellTexts(cells)
|
||||
|
||||
entry := GradeEntry{
|
||||
entry := models.GradeEntry{
|
||||
Term: safeGet(texts, 0),
|
||||
Code: safeGet(texts, 1),
|
||||
Name: safeGet(texts, 2),
|
||||
|
||||
Reference in New Issue
Block a user