refactor(grpc): restructure task handling and migrate to slog
All checks were successful
Build / build (push) Successful in 5m28s
All checks were successful
Build / build (push) Successful in 5m28s
Refactor the gRPC client and task handling logic to improve modularity, error handling, and observability. - Split `grpc/handler.go` into specialized handler files (login, classroom, exams, grades, schedule) to reduce complexity. - Replace standard `log` with `log/slog` throughout the project for structured logging. - Refactor `grpc/client.go` to use a thread-safe connection management pattern with `sync.RWMutex`. - Remove the legacy `service` package and HTTP server implementation, transitioning the application to a pure gRPC runner mode. - Clean up `config` and `pkg/errors` to remove unused utilities and simplify the API. - Improve error reporting in parsers and HTTP clients by checking response status codes.
This commit is contained in:
@@ -2,6 +2,7 @@ package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -19,19 +20,20 @@ type EmptyClassroomEntry struct {
|
||||
Weeks string `json:"weeks"`
|
||||
}
|
||||
|
||||
type BuildingInfo struct {
|
||||
type buildingInfo struct {
|
||||
DM string `json:"dm"`
|
||||
MC string `json:"mc"`
|
||||
}
|
||||
|
||||
type VenueInfo struct {
|
||||
type venueInfo struct {
|
||||
DM string `json:"dm"`
|
||||
MC string `json:"mc"`
|
||||
}
|
||||
|
||||
func FetchEmptyClassrooms(httpClient *http.Client, semester, weekStart, weekEnd, campusCode string) ([]EmptyClassroomEntry, error) {
|
||||
url := config.BaseURL + "/kjscx/queryKjs"
|
||||
fmt.Printf("[空教室] 正在获取空教室数据 (学期=%s, 周次=%s-%s)...\n", semester, weekStart, weekEnd)
|
||||
url := config.Get().BaseURL + "/kjscx/queryKjs"
|
||||
log := slog.With("semester", semester, "week_start", weekStart, "week_end", weekEnd)
|
||||
log.Info("获取空教室数据")
|
||||
|
||||
// First GET to establish session
|
||||
resp, err := httpClient.Get(url)
|
||||
@@ -57,21 +59,25 @@ func FetchEmptyClassrooms(httpClient *http.Client, semester, weekStart, weekEnd,
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
entries, err := ParseEmptyClassroomHTML(html, semester, weekStart, weekEnd)
|
||||
entries, err := parseEmptyClassroomHTML(html, semester, weekStart, weekEnd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("[空教室] 共获取 %d 条空教室记录\n", len(entries))
|
||||
log.Info("空教室数据获取完成", "count", len(entries))
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func ParseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]EmptyClassroomEntry, error) {
|
||||
func parseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]EmptyClassroomEntry, error) {
|
||||
if html == "" {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -193,9 +199,9 @@ func ParseEmptyClassroomHTML(html, semester, weekStart, weekEnd string) ([]Empty
|
||||
return classrooms, nil
|
||||
}
|
||||
|
||||
func FetchBuildings(httpClient *http.Client, campusCode string) ([]BuildingInfo, error) {
|
||||
url := config.BaseURL + "/kjscx/queryJxlListBySjid"
|
||||
fmt.Printf("[空教室] 正在获取楼号列表 (校区=%s)...\n", campusCode)
|
||||
func fetchBuildings(httpClient *http.Client, campusCode string) ([]buildingInfo, error) {
|
||||
url := config.Get().BaseURL + "/kjscx/queryJxlListBySjid"
|
||||
slog.Info("获取楼号列表", "campus", campusCode)
|
||||
|
||||
data := fmt.Sprintf("id=%s", campusCode)
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(data))
|
||||
@@ -204,7 +210,7 @@ func FetchBuildings(httpClient *http.Client, campusCode string) ([]BuildingInfo,
|
||||
}
|
||||
|
||||
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
|
||||
req.Header.Set("Referer", config.BaseURL+"/kjscx/queryKjs")
|
||||
req.Header.Set("Referer", config.Get().BaseURL+"/kjscx/queryKjs")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -212,11 +218,11 @@ func FetchBuildings(httpClient *http.Client, campusCode string) ([]BuildingInfo,
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("获取楼号列表失败, 状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var buildings []BuildingInfo
|
||||
var buildings []buildingInfo
|
||||
if err := decodeJSONResponse(resp, &buildings); err != nil {
|
||||
return nil, fmt.Errorf("解析楼号列表失败: %w", err)
|
||||
}
|
||||
@@ -224,9 +230,9 @@ func FetchBuildings(httpClient *http.Client, campusCode string) ([]BuildingInfo,
|
||||
return buildings, nil
|
||||
}
|
||||
|
||||
func FetchVenues(httpClient *http.Client, buildingCode string) ([]VenueInfo, error) {
|
||||
url := config.BaseURL + "/kjscx/queryJxcdListBySjid"
|
||||
fmt.Printf("[空教室] 正在获取场地列表 (楼号=%s)...\n", buildingCode)
|
||||
func fetchVenues(httpClient *http.Client, buildingCode string) ([]venueInfo, error) {
|
||||
url := config.Get().BaseURL + "/kjscx/queryJxcdListBySjid"
|
||||
slog.Info("获取场地列表", "building", buildingCode)
|
||||
|
||||
data := fmt.Sprintf("id=%s", buildingCode)
|
||||
req, err := http.NewRequest("POST", url, strings.NewReader(data))
|
||||
@@ -235,7 +241,7 @@ func FetchVenues(httpClient *http.Client, buildingCode string) ([]VenueInfo, err
|
||||
}
|
||||
|
||||
req.Header = client.GetHeadersWithContentType("application/x-www-form-urlencoded")
|
||||
req.Header.Set("Referer", config.BaseURL+"/kjscx/queryKjs")
|
||||
req.Header.Set("Referer", config.Get().BaseURL+"/kjscx/queryKjs")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
@@ -243,11 +249,11 @@ func FetchVenues(httpClient *http.Client, buildingCode string) ([]VenueInfo, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("获取场地列表失败, 状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var venues []VenueInfo
|
||||
var venues []venueInfo
|
||||
if err := decodeJSONResponse(resp, &venues); err != nil {
|
||||
return nil, fmt.Errorf("解析场地列表失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -40,8 +41,9 @@ func examTypeToCode(t ExamType) string {
|
||||
}
|
||||
|
||||
func FetchExams(httpClient *http.Client, examType ExamType) ([]ExamEntry, error) {
|
||||
examURL := config.BaseURL + "/kscx/queryKsForXs"
|
||||
fmt.Printf("[考试] 正在获取考试时间数据 (%s)...\n", examType)
|
||||
examURL := config.Get().BaseURL + "/kscx/queryKsForXs"
|
||||
log := slog.With("exam_type", string(examType))
|
||||
log.Info("获取考试时间数据")
|
||||
|
||||
data := fmt.Sprintf("xnxq=&kssjd=%s", examTypeToCode(examType))
|
||||
req, err := http.NewRequest("POST", examURL, strings.NewReader(data))
|
||||
@@ -57,34 +59,43 @@ func FetchExams(httpClient *http.Client, examType ExamType) ([]ExamEntry, error)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
exams, err := ParseExamTimeHTML(html, examType)
|
||||
exams, err := parseExamTimeHTML(html, examType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("[考试] 共获取 %d 条考试记录 (%s)\n", len(exams), examType)
|
||||
log.Info("考试数据获取完成", "exam_count", len(exams))
|
||||
return exams, nil
|
||||
}
|
||||
|
||||
func FetchAllExams(httpClient *http.Client) ([]ExamEntry, error) {
|
||||
var allExams []ExamEntry
|
||||
var lastErr error
|
||||
for _, et := range []ExamType{ExamTypeFinal, ExamTypeMidterm, ExamTypeMakeup} {
|
||||
exams, err := FetchExams(httpClient, et)
|
||||
if err != nil {
|
||||
fmt.Printf("[考试] 获取%s考试失败: %v\n", et, err)
|
||||
slog.Warn("获取考试数据失败", "exam_type", string(et), "error", err)
|
||||
lastErr = err
|
||||
continue
|
||||
}
|
||||
allExams = append(allExams, exams...)
|
||||
}
|
||||
if len(allExams) == 0 && lastErr != nil {
|
||||
return nil, fmt.Errorf("所有考试类型获取均失败: %w", lastErr)
|
||||
}
|
||||
return allExams, nil
|
||||
}
|
||||
|
||||
func ParseExamTimeHTML(html string, examType ExamType) ([]ExamEntry, error) {
|
||||
func parseExamTimeHTML(html string, examType ExamType) ([]ExamEntry, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -26,17 +24,11 @@ type weekRoomEntry struct {
|
||||
}
|
||||
|
||||
// 获取课表
|
||||
func FetchSchedule(httpClient *http.Client) ([]models.Course, error) {
|
||||
return FetchScheduleWithClient(httpClient, config.Semester)
|
||||
}
|
||||
|
||||
// FetchScheduleWithClient 使用指定的 HTTP 客户端和学期获取课表
|
||||
func FetchScheduleWithClient(httpClient *http.Client, semester string) ([]models.Course, error) {
|
||||
fmt.Println("[5] 获取课表数据...")
|
||||
|
||||
// POST请求获取课表
|
||||
data := fmt.Sprintf("xnxq=%s", semester)
|
||||
req, err := http.NewRequest("POST", config.BaseURL+"/kbcx/queryGrkb", strings.NewReader(data))
|
||||
req, err := http.NewRequest("POST", config.Get().BaseURL+"/kbcx/queryGrkb", strings.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -54,35 +46,7 @@ func FetchScheduleWithClient(httpClient *http.Client, semester string) ([]models
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析课表
|
||||
courses, err := ParseScheduleHTML(string(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf(" 共提取 %d 门课程\n", len(courses))
|
||||
|
||||
// 输出到JSON
|
||||
jsonData, err := os.Create("schedule_result.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer jsonData.Close()
|
||||
|
||||
encoder := json.NewEncoder(jsonData)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(courses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println(" JSON结果已保存到: schedule_result.json")
|
||||
|
||||
// 打印课程信息
|
||||
for i, course := range courses {
|
||||
fmt.Printf(" 课程 %d: %s - %s\n", i+1, course.Name, course.Teacher)
|
||||
}
|
||||
|
||||
return courses, nil
|
||||
return ParseScheduleHTML(string(body))
|
||||
}
|
||||
|
||||
// 解析课表HTML
|
||||
@@ -354,27 +318,6 @@ func buildCourseKey(name, teacher, room string) string {
|
||||
return strings.TrimSpace(name) + "|" + strings.TrimSpace(teacher) + "|" + strings.TrimSpace(room)
|
||||
}
|
||||
|
||||
// extractWeekInfo 提取一段文本中的完整周次片段
|
||||
// 例如:于战华[15]周,[2-14]周 -> before=于战华, weeks=[15]周,[2-14]周, after=
|
||||
func extractWeekInfo(text string) (beforeWeek, weeksPart, afterWeek string, ok bool) {
|
||||
indexes := weekTokenRegexp.FindAllStringIndex(text, -1)
|
||||
if len(indexes) == 0 {
|
||||
return "", "", "", false
|
||||
}
|
||||
|
||||
firstStart := indexes[0][0]
|
||||
beforeWeek = strings.TrimSpace(text[:firstStart])
|
||||
|
||||
parts := weekTokenRegexp.FindAllString(text, -1)
|
||||
weeksPart = strings.Join(parts, ",")
|
||||
|
||||
// 周次后(含中间)非周次内容作为教室候选
|
||||
tail := strings.TrimSpace(text[firstStart:])
|
||||
afterWeek = strings.TrimSpace(weekTokenRegexp.ReplaceAllString(tail, ""))
|
||||
afterWeek = strings.Trim(afterWeek, ",, ")
|
||||
return beforeWeek, weeksPart, afterWeek, true
|
||||
}
|
||||
|
||||
// extractWeekEntries 将一段文本拆成多个“周次-地点”片段
|
||||
// 例如:李鹏程[2-8]周N楼-335,[9]周N楼-331
|
||||
// 会拆成:
|
||||
|
||||
@@ -2,6 +2,7 @@ package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -39,16 +40,17 @@ type GPASummary struct {
|
||||
Semesters string
|
||||
}
|
||||
|
||||
func FetchGrades(httpClient *http.Client,ExamType ScoreType) ([]GradeEntry, error) {
|
||||
func FetchGrades(httpClient *http.Client, examType ScoreType) ([]GradeEntry, error) {
|
||||
var gradeURL string
|
||||
switch ExamType {
|
||||
switch examType {
|
||||
case ScoreTypeMidterm:
|
||||
gradeURL = config.BaseURL + "/cjcx/queryQzcj"
|
||||
gradeURL = config.Get().BaseURL + "/cjcx/queryQzcj"
|
||||
default:
|
||||
gradeURL = config.BaseURL + "/cjcx/queryQmcj"
|
||||
gradeURL = config.Get().BaseURL + "/cjcx/queryQmcj"
|
||||
}
|
||||
|
||||
fmt.Printf("[成绩] 正在获取成绩数据 (%s)...\n", ExamType)
|
||||
log := slog.With("exam_type", string(examType))
|
||||
log.Info("获取成绩数据")
|
||||
|
||||
req, err := http.NewRequest("GET", gradeURL, nil)
|
||||
if err != nil {
|
||||
@@ -62,23 +64,27 @@ func FetchGrades(httpClient *http.Client,ExamType ScoreType) ([]GradeEntry, erro
|
||||
}
|
||||
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)
|
||||
grades, err := parseGradesHTML(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("[成绩] 共获取 %d 条成绩记录\n", len(grades))
|
||||
log.Info("成绩获取完成", "grade_count", len(grades))
|
||||
return grades, nil
|
||||
}
|
||||
|
||||
func FetchGPA(httpClient *http.Client) (*GPASummary, []GradeEntry, error) {
|
||||
gpaURL := config.BaseURL + "/xfj/queryListXfj"
|
||||
fmt.Println("[学分绩] 正在获取学分绩数据...")
|
||||
gpaURL := config.Get().BaseURL + "/xfj/queryListXfj"
|
||||
slog.Info("获取学分绩数据")
|
||||
|
||||
req, err := http.NewRequest("GET", gpaURL, nil)
|
||||
if err != nil {
|
||||
@@ -92,21 +98,25 @@ func FetchGPA(httpClient *http.Client) (*GPASummary, []GradeEntry, error) {
|
||||
}
|
||||
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)
|
||||
summary, grades, err := parseGPAHTML(html)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
fmt.Printf("[学分绩] 共获取 %d 门课程明细\n", len(grades))
|
||||
slog.Info("学分绩获取完成", "grade_count", len(grades))
|
||||
return summary, grades, nil
|
||||
}
|
||||
|
||||
func ParseGradesHTML(html string) ([]GradeEntry, error) {
|
||||
func parseGradesHTML(html string) ([]GradeEntry, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -173,7 +183,7 @@ func ParseGradesHTML(html string) ([]GradeEntry, error) {
|
||||
return grades, nil
|
||||
}
|
||||
|
||||
func ParseGPAHTML(html string) (*GPASummary, []GradeEntry, error) {
|
||||
func parseGPAHTML(html string) (*GPASummary, []GradeEntry, error) {
|
||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(html))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -254,4 +264,4 @@ func safeGet(arr []string, idx int) string {
|
||||
return arr[idx]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user