refactor: 重构代码架构并使用 Modern Go 特性

- 重命名模块 jwts -> schedule_converter
- 新增 service 包,统一业务逻辑层
- 新增 pkg/logger 包,使用 slog 结构化日志
- 新增 pkg/errors 包,统一错误处理
- 拆分 captcha 包,图片识别移至 service/recognizer
- 清理 models 包,只保留数据结构定义
- 消除 grpc/handler.go 中的重复代码
- 简化 main.go,使用 slog 和 context
- 配置支持单例模式和结构化配置
- 使用 any 替代 interface{}
- 使用 sync.Map 替代 map + RWMutex
This commit is contained in:
lafay
2026-03-16 13:40:53 +08:00
parent 294151cd40
commit 74438c1f7f
20 changed files with 1183 additions and 602 deletions

View File

@@ -8,7 +8,7 @@ import (
"sync"
"time"
pb "jwts/proto/runner"
pb "schedule_converter/proto/runner"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

View File

@@ -1,262 +1,161 @@
package grpc
import (
"context"
"encoding/json"
"fmt"
"log"
"log/slog"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"sync"
"time"
pb "jwts/proto/runner"
pb "schedule_converter/proto/runner"
"jwts/auth"
"jwts/captcha"
"jwts/config"
"jwts/models"
"jwts/parser"
"schedule_converter/auth"
"schedule_converter/captcha"
"schedule_converter/client"
"schedule_converter/config"
"schedule_converter/models"
"schedule_converter/parser"
apperr "schedule_converter/pkg/errors"
)
// TaskHandler 任务处理器
type TaskHandler struct {
cancelledTasks map[string]bool
mu sync.RWMutex
cancelledTasks sync.Map
}
// NewTaskHandler 创建新的任务处理器
func NewTaskHandler() *TaskHandler {
return &TaskHandler{
cancelledTasks: make(map[string]bool),
}
return &TaskHandler{}
}
// Execute 执行任务
func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult {
log := slog.With("task_id", task.TaskId, "task_type", task.Type.String())
log.Info("开始执行任务")
result := &pb.TaskResult{
TaskId: task.TaskId,
CompletedAt: time.Now().UnixMilli(),
}
// 检查任务是否已取消
if h.IsCancelled(task.TaskId) {
result.Status = pb.TaskStatus_TASK_STATUS_CANCELLED
result.ErrorMessage = "任务已取消"
log.Info("任务已取消")
return result
}
var err error
var data []byte
switch task.Type {
case pb.TaskType_TASK_TYPE_GET_SCHEDULE:
data, err := h.getSchedule(task.Payload)
if err != nil {
result.Status = pb.TaskStatus_TASK_STATUS_FAILED
result.ErrorMessage = err.Error()
} else {
result.Status = pb.TaskStatus_TASK_STATUS_SUCCESS
result.Data = data
}
data, err = h.getSchedule(context.Background(), task.Payload)
case pb.TaskType_TASK_TYPE_LOGIN:
data, err := h.login(task.Payload)
if err != nil {
result.Status = pb.TaskStatus_TASK_STATUS_FAILED
result.ErrorMessage = err.Error()
} else {
result.Status = pb.TaskStatus_TASK_STATUS_SUCCESS
result.Data = data
}
data, err = h.login(context.Background(), task.Payload)
default:
result.Status = pb.TaskStatus_TASK_STATUS_FAILED
result.ErrorMessage = fmt.Sprintf("未知的任务类型: %v", task.Type)
log.Warn("未知的任务类型")
return result
}
if err != nil {
result.Status = pb.TaskStatus_TASK_STATUS_FAILED
result.ErrorMessage = err.Error()
log.Error("任务执行失败", "error", err)
} else {
result.Status = pb.TaskStatus_TASK_STATUS_SUCCESS
result.Data = data
log.Info("任务执行成功")
}
return result
}
// CancelTask 取消任务
func (h *TaskHandler) CancelTask(taskID string) {
h.mu.Lock()
defer h.mu.Unlock()
h.cancelledTasks[taskID] = true
log.Printf("任务已标记为取消: %s", taskID)
h.cancelledTasks.Store(taskID, true)
slog.Info("任务已标记为取消", "task_id", taskID)
}
// IsCancelled 检查任务是否已取消
func (h *TaskHandler) IsCancelled(taskID string) bool {
h.mu.RLock()
defer h.mu.RUnlock()
return h.cancelledTasks[taskID]
v, ok := h.cancelledTasks.Load(taskID)
return ok && v.(bool)
}
// getSchedule 获取课表
func (h *TaskHandler) getSchedule(payload []byte) ([]byte, error) {
func (h *TaskHandler) getSchedule(ctx context.Context, payload []byte) ([]byte, error) {
var req pb.GetSchedulePayload
if err := json.Unmarshal(payload, &req); err != nil {
return nil, fmt.Errorf("解析任务载荷失败: %w", err)
return nil, apperr.New("parse_payload", err, "解析任务载荷失败")
}
semester := strings.TrimSpace(req.Semester)
if semester == "" {
semester = config.Semester
}
log := slog.With("username", req.Username, "semester", req.Semester)
log.Info("获取课表任务")
log.Printf("获取课表: 用户=%s, 学期=%s", req.Username, semester)
// 创建新的 HTTP 客户端
jar, err := cookiejar.New(nil)
httpClient, err := createHTTPClient()
if err != nil {
return nil, fmt.Errorf("创建 cookie jar 失败: %w", err)
}
httpClient := &http.Client{
Jar: jar,
return nil, err
}
// 设置 TWFID cookie (使用默认配置)
setTWFIDCookie(httpClient, config.BaseURL, "")
cfg := config.Get()
setTWFIDCookie(httpClient, cfg.BaseURL, "")
// 步骤1: 获取初始 Cookie
if err := getInitialCookies(httpClient); err != nil {
return nil, fmt.Errorf("获取初始 Cookie 失败: %w", err)
}
// 重试登录
maxRetries := 3
var loginSuccess bool
for attempt := 1; attempt <= maxRetries; attempt++ {
log.Printf("尝试登录 (第%d次)...", attempt)
// 获取并识别验证码
captchaCode, err := captcha.GetAndRecognize(httpClient)
if err != nil {
log.Printf(" 验证码获取失败: %v", err)
continue
}
// 登录
success, err := loginWithCredentials(httpClient, captchaCode, req.Username, req.Password)
if err != nil {
log.Printf(" 登录失败: %v", err)
continue
}
if success {
loginSuccess = true
break
}
log.Println(" 登录失败3秒后重试...")
time.Sleep(3 * time.Second)
}
if !loginSuccess {
return nil, fmt.Errorf("登录失败,已重试 %d 次", maxRetries)
}
// 获取课表
courses, err := parser.FetchScheduleWithClient(httpClient, semester)
courses, err := executeLoginAndFetchSchedule(ctx, httpClient, req.Username, req.Password, req.Semester)
if err != nil {
return nil, fmt.Errorf("获取课表失败: %w", err)
return nil, err
}
// 构建结果
result := &pb.ScheduleResultData{
StudentId: req.Username,
Semester: semester,
Semester: req.Semester,
Courses: convertCourses(courses),
}
log.Info("课表获取成功", "course_count", len(courses))
return json.Marshal(result)
}
// login 登录验证
func (h *TaskHandler) login(payload []byte) ([]byte, error) {
func (h *TaskHandler) login(ctx context.Context, payload []byte) ([]byte, error) {
var req pb.LoginPayload
if err := json.Unmarshal(payload, &req); err != nil {
return nil, fmt.Errorf("解析任务载荷失败: %w", err)
return nil, apperr.New("parse_payload", err, "解析任务载荷失败")
}
log.Printf("登录验证: 用户=%s", req.Username)
log := slog.With("username", req.Username)
log.Info("登录任务")
// 创建新的 HTTP 客户端
httpClient, err := createHTTPClient()
if err != nil {
return nil, err
}
cfg := config.Get()
setTWFIDCookie(httpClient, cfg.BaseURL, req.Twfid)
if err := executeLogin(ctx, httpClient, req.Username, req.Password); err != nil {
return nil, err
}
log.Info("登录成功")
return json.Marshal(map[string]any{
"success": true,
"message": "登录成功",
})
}
func createHTTPClient() (*http.Client, error) {
jar, err := cookiejar.New(nil)
if err != nil {
return nil, fmt.Errorf("创建 cookie jar 失败: %w", err)
return nil, apperr.New("create_jar", err, "创建 cookie jar 失败")
}
httpClient := &http.Client{
Jar: jar,
}
// 设置 TWFID cookie
setTWFIDCookie(httpClient, config.BaseURL, req.Twfid)
// 步骤1: 获取初始 Cookie
if err := getInitialCookies(httpClient); err != nil {
return nil, fmt.Errorf("获取初始 Cookie 失败: %w", err)
}
// 重试登录
maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
log.Printf("尝试登录 (第%d次)...", attempt)
// 获取并识别验证码
captchaCode, err := captcha.GetAndRecognize(httpClient)
if err != nil {
log.Printf(" 验证码获取失败: %v", err)
continue
}
// 登录
success, err := loginWithCredentials(httpClient, captchaCode, req.Username, req.Password)
if err != nil {
log.Printf(" 登录失败: %v", err)
continue
}
if success {
return json.Marshal(map[string]interface{}{
"success": true,
"message": "登录成功",
})
}
log.Println(" 登录失败3秒后重试...")
time.Sleep(3 * time.Second)
}
return nil, fmt.Errorf("登录失败,已重试 %d 次", maxRetries)
return &http.Client{Jar: jar}, nil
}
// getInitialCookies 获取初始 Cookie
func getInitialCookies(httpClient *http.Client) error {
log.Println("[1] 访问登录页面获取初始 Cookie...")
req, err := http.NewRequest("GET", config.BaseURL+"/loginNOCAS", nil)
if err != nil {
return err
}
req.Header = getHeaders()
resp, err := httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
log.Printf(" 登录页面状态码: %d", resp.StatusCode)
return nil
}
// loginWithCredentials 使用凭据登录
func loginWithCredentials(httpClient *http.Client, captchaCode, username, password string) (bool, error) {
return auth.LoginWithClient(httpClient, captchaCode, username, password)
}
// setTWFIDCookie 设置 TWFID Cookie
func setTWFIDCookie(httpClient *http.Client, baseURL, twfid string) {
cfg := config.Get()
if twfid == "" {
twfid = config.TWFID
twfid = cfg.TWFID
}
u, _ := url.Parse(baseURL)
httpClient.Jar.SetCookies(u, []*http.Cookie{
@@ -264,38 +163,84 @@ func setTWFIDCookie(httpClient *http.Client, baseURL, twfid string) {
})
}
// getHeaders 获取请求头
func getHeaders() http.Header {
return http.Header{
"User-Agent": []string{"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"},
"Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"},
"Accept-Language": []string{"zh-CN,zh;q=0.9,en;q=0.8"},
func executeLogin(ctx context.Context, httpClient *http.Client, username, password string) error {
log := slog.With("username", username)
if err := client.GetInitialCookiesWithClient(httpClient); err != nil {
return apperr.New("get_cookies", err, "获取初始 Cookie 失败")
}
cfg := config.Get()
_ = cfg.Recognizer.Timeout
for attempt := 1; attempt <= 3; attempt++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
log.Info("尝试登录", "attempt", attempt, "max_retries", 3)
captchaCode, err := captcha.GetAndRecognize(httpClient)
if err != nil {
log.Warn("验证码获取失败", "error", err, "attempt", attempt)
time.Sleep(3 * time.Second)
continue
}
success, err := auth.LoginWithClient(httpClient, captchaCode, username, password)
if err != nil {
log.Warn("登录请求失败", "error", err, "attempt", attempt)
time.Sleep(3 * time.Second)
continue
}
if success {
return nil
}
log.Warn("登录验证失败", "attempt", attempt)
time.Sleep(3 * time.Second)
}
return apperr.ErrLoginFailed
}
func executeLoginAndFetchSchedule(ctx context.Context, httpClient *http.Client, username, password, semester string) ([]models.Course, error) {
if err := executeLogin(ctx, httpClient, username, password); err != nil {
return nil, err
}
cfg := config.Get()
if semester == "" {
semester = cfg.Semester
}
return parser.FetchScheduleWithClient(httpClient, semester)
}
// convertCourses 转换课程格式
func convertCourses(courses []models.Course) []*pb.Course {
var result []*pb.Course
for _, c := range courses {
var schedule []*pb.ScheduleTime
for _, s := range c.Schedule {
schedule = append(schedule, &pb.ScheduleTime{
result := make([]*pb.Course, len(courses))
for i, c := range courses {
schedule := make([]*pb.ScheduleTime, len(c.Schedule))
for j, s := range c.Schedule {
schedule[j] = &pb.ScheduleTime{
Day: s.Day,
Sections: convertToInt32(s.Section),
Weeks: convertToInt32(s.Weeks),
})
}
}
result = append(result, &pb.Course{
result[i] = &pb.Course{
Name: c.Name,
Teacher: c.Teacher,
Room: c.Room,
Schedule: schedule,
})
}
}
return result
}
// convertToInt32 将 []int 转换为 []int32
func convertToInt32(arr []int) []int32 {
result := make([]int32, len(arr))
for i, v := range arr {