Files
wk-backend/internal/ckwk/api.go
zhilv b0db64bd7b feat: 添加定时系统
- 实现定时任务
2026-03-26 22:59:58 +08:00

404 lines
10 KiB
Go

package ckwk
import (
"ckwk/internal/conf"
"ckwk/pkg/common"
"ckwk/pkg/log"
"ckwk/pkg/request"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"regexp"
"strconv"
"strings"
"github.com/antchfx/htmlquery"
"go.uber.org/zap"
"resty.dev/v3"
)
type WK struct {
Username string `json:"username"`
Password string `json:"password"`
Host string `json:"host"`
Req *resty.Client
Cookies []*http.Cookie
LoginRegexp *regexp.Regexp
CourseIDRegexp *regexp.Regexp
TimeRegexp *regexp.Regexp
}
func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
hasAuth := (username != "" && password != "") || len(cookies) > 0
if host == "" || !hasAuth {
return nil
}
req := request.NewClient(&request.Config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
Proxy: "http://127.0.0.1:9000",
VerifySSL: false,
})
if len(cookies) > 0 {
req.SetCookies(cookies)
}
wk := &WK{
Username: username,
Password: password,
Host: host,
Req: req,
Cookies: cookies,
LoginRegexp: regexp.MustCompile("var data =(.*?);"),
CourseIDRegexp: regexp.MustCompile(`\?courseId=(\d+)`),
TimeRegexp: regexp.MustCompile(`\d{4}-\d{2}-\d{2}`),
}
if len(cookies) == 0 && username != "" {
wk.Login()
}
return wk
}
// Cookies: returns cookies
func (wk *WK) Cookie() []*http.Cookie {
return wk.Cookies
}
// SetCookies: returns cookies
func (wk *WK) SetCookies(cs []*http.Cookie) {
wk.Cookies = cs
wk.Req.SetCookies(cs)
}
// Code: Get Verify Code
func (wk *WK) Code() (string, error) {
resp, err := wk.Req.
R().
SetQueryParam("r", fmt.Sprint(common.RandFloat64())).
Get(fmt.Sprintf("https://%s/service/code", wk.Host))
if err != nil {
return "", fmt.Errorf("获取验证码失败: %w", err)
}
if resp.StatusCode() != 200 {
return "", fmt.Errorf("获取验证码失败: code: %d", resp.StatusCode())
}
var result CodeResp
_, err = wk.Req.
R().
SetFormData(map[string]string{
"image": base64.StdEncoding.EncodeToString(resp.Bytes()),
"probability": "false",
"png_fix": "false",
}).
SetResult(&result).
Post(fmt.Sprintf("%s/ocr", conf.DdddOCR))
if err != nil {
return "", fmt.Errorf("获取验证码验证结果失败: %w", err)
}
if result.Code != 200 {
return "", fmt.Errorf("ocr 失败: code: %d, message: %s", result.Code, result.Message)
}
return result.Data, nil
}
// Login: Login WebSite
func (wk *WK) Login() (bool, error) {
yzm := ""
for i := 1; i <= 3; i++ {
yzm, _ = wk.Code()
if yzm != "" {
break
}
log.Warnf("第 %d 次获取验证码失败, 正在重试...\n", i)
}
if yzm == "" {
return false, fmt.Errorf("以达到最大重试次数,验证码获取失败,登录终止。")
}
resp, err := wk.Req.
R().
SetFormData(map[string]string{
"username": wk.Username,
"password": wk.Password,
"code": yzm,
"redirect": "",
}).
Post(fmt.Sprintf("https://%s/user/login", wk.Host))
if err != nil {
return false, fmt.Errorf("请求登录失败: %w", err)
}
matchs := wk.LoginRegexp.FindStringSubmatch(string(resp.Bytes()))
if len(matchs) <= 1 {
return false, fmt.Errorf("没有找到匹配字符串")
}
var result LoginResp
err = json.Unmarshal([]byte(matchs[1]), &result)
if err != nil {
return false, fmt.Errorf("解析 json data 失败: %w", err)
}
if !result.Status {
return false, fmt.Errorf("登录失败: %s", result.Msg)
}
wk.SetCookies(resp.Cookies())
log.Info("登录成功", zap.Any("cookies", wk.Cookies))
return true, nil
}
// CourseParse: 课程解析
func (wk *WK) CourseParse(content string) ([]Course, error) {
courses := make([]Course, 0)
doc, err := htmlquery.Parse(strings.NewReader(content))
if err != nil {
return courses, fmt.Errorf("解析 HTML 失败: %w", err)
}
itemsNode := htmlquery.Find(doc, `//div[@class="user-course"]/div[@class="item"]`)
if len(itemsNode) < 1 {
return courses, nil
// return courses, fmt.Errorf("未解析到课程")
}
for _, node := range itemsNode {
aNode := htmlquery.FindOne(node, `.//div[@class="name"]/a`)
if aNode == nil {
continue
}
href := htmlquery.SelectAttr(aNode, "href")
match := wk.CourseIDRegexp.FindStringSubmatch(href)
var id int
if len(match) > 1 {
idVal, err := strconv.Atoi(match[1])
if err == nil {
id = idVal
} else {
log.Warn("ID 转换失败", zap.Error(err))
}
}
tagsNode := htmlquery.FindOne(node, `.//div[@class="tags"]/span`)
progressNode := htmlquery.FindOne(node, `.//div[@class="progress"]/div[@class="txt"]`)
timeNode := htmlquery.FindOne(node, `.//div[@class="time"]`)
var startTime, stopTime string
if timeNode != nil {
fullText := htmlquery.InnerText(timeNode)
dates := wk.TimeRegexp.FindAllString(fullText, -1)
if len(dates) >= 2 {
startTime = dates[0]
stopTime = dates[1]
}
}
var credit float32
creditNode := htmlquery.FindOne(node, `.//div[@class="note"]/div[@class="number"]/span`)
creditStr := strings.TrimSpace(htmlquery.InnerText(creditNode))
creditVal, err := strconv.ParseFloat(creditStr, 32)
if err == nil {
credit = float32(creditVal)
} else {
log.Warn("课程学分转换失败", zap.Error(err))
}
typeNode := htmlquery.FindOne(node, `.//div[@class="note"]/div[@class="kind"]/span`)
course := Course{
ID: id,
Name: htmlquery.InnerText(aNode),
Teacher: htmlquery.InnerText(tagsNode),
Progress: htmlquery.InnerText(progressNode),
StartTime: startTime,
StopTime: stopTime,
Credit: credit,
Type: htmlquery.InnerText(typeNode),
}
courses = append(courses, course)
}
return courses, nil
}
// CourseGet: 课程获取
func (wk *WK) CourseGet(kind CourseKind) ([]Course, error) {
var courses []Course
resp, err := wk.Req.
R().
SetQueryParam("kind", string(kind)).
Get(fmt.Sprintf("https://%s/user/index", wk.Host))
if err != nil {
return courses, fmt.Errorf("获取课程失败: %w", err)
}
courses, err = wk.CourseParse(string(resp.Bytes()))
if err != nil {
return courses, fmt.Errorf("解析课程失败: %w", err)
}
return courses, nil
}
// UserParse: 用户信息解析
func (wk *WK) UserInfoParse(content string) (User, error) {
var user User
doc, err := htmlquery.Parse(strings.NewReader(content))
if err != nil {
return user, fmt.Errorf("解析用户信息失败: %w", err)
}
userInfoNode := htmlquery.FindOne(doc, `//div[@class="useredata-form"]`)
if userInfoNode == nil {
return user, fmt.Errorf("未解析到用户信息")
}
nameNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][1]/div[@class="txt"]/text()`)
if nameNode != nil {
user.Name = strings.TrimSpace(htmlquery.InnerText(nameNode))
}
idNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][2]/div[@class="txt"]`)
if idNode != nil {
user.ID = strings.TrimSpace(htmlquery.InnerText(idNode))
}
deptNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][3]/div[@class="txt"]`)
if deptNode != nil {
user.Dept = strings.TrimSpace(htmlquery.InnerText(deptNode))
}
classNode := htmlquery.FindOne(userInfoNode, `.//div[@class="item"][4]/div[@class="txt"]`)
if classNode != nil {
user.Class = strings.TrimSpace(htmlquery.InnerText(classNode))
}
genderNode := htmlquery.FindOne(userInfoNode, `.//select[@id="gender"]/option[@selected="selected"]`)
if genderNode != nil {
user.Gender = htmlquery.SelectAttr(genderNode, "value")
}
return user, nil
}
// UserGet: 用户信息获取
func (wk *WK) UserInfoGet() (User, error) {
var user User
resp, err := wk.Req.
R().
Get(fmt.Sprintf("https://%s/user/member", wk.Host))
if err != nil {
return user, fmt.Errorf("获取用户信息页面失败: %w", err)
}
user, err = wk.UserInfoParse(string(resp.Bytes()))
if err != nil {
return user, err
}
wk.Username = user.ID
return user, nil
}
// Online: 保持账号状态
func (wk *WK) Online() (bool, error) {
resp, err := wk.Req.
R().
SetHeaders(map[string]string{
"x-requested-with": "XMLHttpRequest",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate, br, zstd",
}).
Post(fmt.Sprintf("https://%s/user/online", wk.Host))
if err != nil {
return false, fmt.Errorf("保持账号状态失败: %w", err)
}
log.Info("保持账号状态", zap.Any("resp", string(resp.Bytes())))
return true, nil
}
// Study: 学习
func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
var data map[string]string
switch status {
case StudyStart:
yzm := ""
for i := 1; i <= 3; i++ {
yzm, _ = wk.Code()
if yzm != "" {
break
}
log.Warnf("第 %d 次获取验证码失败, 正在重试...\n", i)
}
if yzm == "" {
return nil, fmt.Errorf("以达到最大重试次数,验证码获取失败,登录终止。")
}
data = map[string]string{
"nodeId": nodeID,
"studyId": "0",
"studyTime": "1",
"code": yzm,
}
case Study:
data = map[string]string{
"nodeId": nodeID,
"studyId": studyID,
"studyTime": studyTime,
}
case StudyOver:
data = map[string]string{
"nodeId": nodeID,
"studyId": studyID,
"studyTime": studyTime,
"close": "1",
}
default:
return nil, fmt.Errorf("传入的学习状态不匹配")
}
resp, err := wk.Req.
R().
SetHeaders(map[string]string{
"x-requested-with": "XMLHttpRequest",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Encoding": "gzip, deflate, br, zstd",
}).
SetFormData(data).
Post(fmt.Sprintf("https://%s/user/node/study", wk.Host))
if err != nil {
return nil, fmt.Errorf("进行学习失败: %w", err)
}
var result StudyResp
if err := json.Unmarshal(resp.Bytes(), &result); err != nil {
return nil, fmt.Errorf("解析响应失败: %w", err)
}
log.Info("学习响应", zap.Any("resp", result))
return &result, nil
}
// GetStudyList: 获取学习记录
func (wk *WK) GetStudyList(courseID, page string) (*AllRecordResp[StudyList], error) {
return GetRecords[StudyList](wk, RecordStudy, courseID, page)
}
// GetExamList: 获取作业记录
// todo
func (wk *WK) GetWorkList(courseID, page string) (*AllRecordResp[ExamList], error) {
return GetRecords[ExamList](wk, RecordStudy, courseID, page)
}
// GetExamList: 获取考试记录
func (wk *WK) GetExamList(courseID, page string) (*AllRecordResp[ExamList], error) {
return GetRecords[ExamList](wk, RecordExam, courseID, page)
}
// GetExamList: 获取讨论记录
// todo
func (wk *WK) GetDiscussList(courseID, page string) (*AllRecordResp[ExamList], error) {
return GetRecords[ExamList](wk, RecordStudy, courseID, page)
}