404 lines
10 KiB
Go
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)
|
|
}
|