Files
schedule_converter/grpc/handler.go
2026-03-13 20:43:44 +08:00

306 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}