init: 第一次提交
- 以实现登录获取个人信息和课程 - 实现了获取视频记录 - 实现了学习接口
This commit is contained in:
402
internal/ckwk/api.go
Normal file
402
internal/ckwk/api.go
Normal file
@@ -0,0 +1,402 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"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("http://localhost:8000/ocr")
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user