package grpc import ( "encoding/json" "fmt" "log" "net/http" "net/http/cookiejar" "net/url" "strings" "sync" "time" pb "jwts/proto/runner" "jwts/auth" "jwts/captcha" "jwts/config" "jwts/models" "jwts/parser" ) // TaskHandler 任务处理器 type TaskHandler struct { cancelledTasks map[string]bool mu sync.RWMutex } // NewTaskHandler 创建新的任务处理器 func NewTaskHandler() *TaskHandler { return &TaskHandler{ cancelledTasks: make(map[string]bool), } } // Execute 执行任务 func (h *TaskHandler) Execute(task *pb.Task) *pb.TaskResult { result := &pb.TaskResult{ TaskId: task.TaskId, CompletedAt: time.Now().UnixMilli(), } // 检查任务是否已取消 if h.IsCancelled(task.TaskId) { result.Status = pb.TaskStatus_TASK_STATUS_CANCELLED result.ErrorMessage = "任务已取消" return result } 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 } 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 } default: result.Status = pb.TaskStatus_TASK_STATUS_FAILED result.ErrorMessage = fmt.Sprintf("未知的任务类型: %v", task.Type) } return result } // CancelTask 取消任务 func (h *TaskHandler) CancelTask(taskID string) { h.mu.Lock() defer h.mu.Unlock() h.cancelledTasks[taskID] = true log.Printf("任务已标记为取消: %s", taskID) } // IsCancelled 检查任务是否已取消 func (h *TaskHandler) IsCancelled(taskID string) bool { h.mu.RLock() defer h.mu.RUnlock() return h.cancelledTasks[taskID] } // getSchedule 获取课表 func (h *TaskHandler) getSchedule(payload []byte) ([]byte, error) { var req pb.GetSchedulePayload if err := json.Unmarshal(payload, &req); err != nil { return nil, fmt.Errorf("解析任务载荷失败: %w", err) } semester := strings.TrimSpace(req.Semester) if semester == "" { semester = config.Semester } log.Printf("获取课表: 用户=%s, 学期=%s", req.Username, semester) // 创建新的 HTTP 客户端 jar, err := cookiejar.New(nil) if err != nil { return nil, fmt.Errorf("创建 cookie jar 失败: %w", err) } httpClient := &http.Client{ Jar: jar, } // 设置 TWFID cookie (使用默认配置) setTWFIDCookie(httpClient, config.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) if err != nil { return nil, fmt.Errorf("获取课表失败: %w", err) } // 构建结果 result := &pb.ScheduleResultData{ StudentId: req.Username, Semester: semester, Courses: convertCourses(courses), } return json.Marshal(result) } // login 登录验证 func (h *TaskHandler) login(payload []byte) ([]byte, error) { var req pb.LoginPayload if err := json.Unmarshal(payload, &req); err != nil { return nil, fmt.Errorf("解析任务载荷失败: %w", err) } log.Printf("登录验证: 用户=%s", req.Username) // 创建新的 HTTP 客户端 jar, err := cookiejar.New(nil) if err != nil { return nil, fmt.Errorf("创建 cookie jar 失败: %w", err) } 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) } // 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) { if twfid == "" { twfid = config.TWFID } u, _ := url.Parse(baseURL) httpClient.Jar.SetCookies(u, []*http.Cookie{ {Name: "TWFID", Value: twfid}, }) } // 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"}, } } // 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{ Day: s.Day, Sections: convertToInt32(s.Section), Weeks: convertToInt32(s.Weeks), }) } result = append(result, &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 { result[i] = int32(v) } return result }