|
|
|
|
@@ -7,17 +7,27 @@ import (
|
|
|
|
|
"ckwk/pkg/request"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"regexp"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
|
|
"github.com/antchfx/htmlquery"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
"resty.dev/v3"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
ErrLoginTimeout = errors.New("登录超时,请重新登录")
|
|
|
|
|
ErrSessionRemoved = errors.New("session_manager 已删除失效会话")
|
|
|
|
|
ErrReloginSkipped = errors.New("当前会话缺少账号密码,无法自动重新登录")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const maxAutoReloginRetries = 3
|
|
|
|
|
|
|
|
|
|
type WK struct {
|
|
|
|
|
Username string `json:"username"`
|
|
|
|
|
Password string `json:"password"`
|
|
|
|
|
@@ -29,6 +39,10 @@ type WK struct {
|
|
|
|
|
LoginRegexp *regexp.Regexp
|
|
|
|
|
CourseIDRegexp *regexp.Regexp
|
|
|
|
|
TimeRegexp *regexp.Regexp
|
|
|
|
|
|
|
|
|
|
authMu sync.Mutex
|
|
|
|
|
sessionID string
|
|
|
|
|
sessionManager *SessionManager
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
|
|
|
|
@@ -40,9 +54,9 @@ func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
|
|
|
|
reqCfg := &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",
|
|
|
|
|
VerifySSL: true,
|
|
|
|
|
Debug: conf.IsDebugMode(),
|
|
|
|
|
Debug: conf.IsRuntimeDebugEnabled(),
|
|
|
|
|
}
|
|
|
|
|
if conf.IsDebugMode() {
|
|
|
|
|
if conf.IsRuntimeDebugEnabled() {
|
|
|
|
|
reqCfg.Proxy = conf.DebugProxy
|
|
|
|
|
reqCfg.VerifySSL = !conf.DebugSkipSSLVerify
|
|
|
|
|
}
|
|
|
|
|
@@ -63,13 +77,41 @@ func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
|
|
|
|
CourseIDRegexp: regexp.MustCompile(`\?courseId=(\d+)`),
|
|
|
|
|
TimeRegexp: regexp.MustCompile(`\d{4}-\d{2}-\d{2}`),
|
|
|
|
|
}
|
|
|
|
|
if len(cookies) == 0 && username != "" {
|
|
|
|
|
wk.Login()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return wk
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) bindSession(sm *SessionManager, sessionID string) {
|
|
|
|
|
wk.sessionManager = sm
|
|
|
|
|
wk.sessionID = sessionID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) prepareRequestClient() {
|
|
|
|
|
if wk == nil || wk.Req == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg := &request.Config{
|
|
|
|
|
UserAgent: request.DefaultUserAgent,
|
|
|
|
|
VerifySSL: true,
|
|
|
|
|
Debug: conf.IsRuntimeDebugEnabled(),
|
|
|
|
|
}
|
|
|
|
|
if conf.IsRuntimeDebugEnabled() {
|
|
|
|
|
cfg.Proxy = conf.DebugProxy
|
|
|
|
|
cfg.VerifySSL = !conf.DebugSkipSSLVerify
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
request.ApplyConfig(wk.Req, cfg)
|
|
|
|
|
if len(wk.Cookies) > 0 {
|
|
|
|
|
wk.Req.SetCookies(wk.Cookies)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) newRequest() *resty.Request {
|
|
|
|
|
wk.prepareRequestClient()
|
|
|
|
|
return wk.Req.R()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cookies: returns cookies
|
|
|
|
|
func (wk *WK) Cookie() []*http.Cookie {
|
|
|
|
|
return wk.Cookies
|
|
|
|
|
@@ -83,8 +125,7 @@ func (wk *WK) SetCookies(cs []*http.Cookie) {
|
|
|
|
|
|
|
|
|
|
// Code: Get Verify Code
|
|
|
|
|
func (wk *WK) Code() (string, error) {
|
|
|
|
|
resp, err := wk.Req.
|
|
|
|
|
R().
|
|
|
|
|
resp, err := wk.newRequest().
|
|
|
|
|
SetQueryParam("r", fmt.Sprint(common.RandFloat64())).
|
|
|
|
|
Get(fmt.Sprintf("https://%s/service/code", wk.Host))
|
|
|
|
|
|
|
|
|
|
@@ -96,8 +137,7 @@ func (wk *WK) Code() (string, error) {
|
|
|
|
|
return "", fmt.Errorf("获取验证码失败: code: %d", resp.StatusCode())
|
|
|
|
|
}
|
|
|
|
|
var result CodeResp
|
|
|
|
|
_, err = wk.Req.
|
|
|
|
|
R().
|
|
|
|
|
_, err = wk.newRequest().
|
|
|
|
|
SetFormData(map[string]string{
|
|
|
|
|
"image": base64.StdEncoding.EncodeToString(resp.Bytes()),
|
|
|
|
|
"probability": "false",
|
|
|
|
|
@@ -128,8 +168,7 @@ func (wk *WK) Login() (bool, error) {
|
|
|
|
|
return false, fmt.Errorf("以达到最大重试次数,验证码获取失败,登录终止。")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := wk.Req.
|
|
|
|
|
R().
|
|
|
|
|
resp, err := wk.newRequest().
|
|
|
|
|
SetFormData(map[string]string{
|
|
|
|
|
"username": wk.Username,
|
|
|
|
|
"password": wk.Password,
|
|
|
|
|
@@ -141,25 +180,50 @@ func (wk *WK) Login() (bool, error) {
|
|
|
|
|
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)
|
|
|
|
|
result, err := wk.parseLoginResp(resp.Bytes())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, fmt.Errorf("解析 json data 失败: %w", err)
|
|
|
|
|
return false, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !result.Status {
|
|
|
|
|
return false, fmt.Errorf("登录失败: %s", result.Msg)
|
|
|
|
|
return false, fmt.Errorf("登录失败: %s", loginErrorMessage(result))
|
|
|
|
|
}
|
|
|
|
|
wk.SetCookies(resp.Cookies())
|
|
|
|
|
log.Info("登录成功", zap.Any("cookies", wk.Cookies))
|
|
|
|
|
log.Info("登录成功", zap.Any("cookies", wk.Req.Cookies()))
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) parseLoginResp(body []byte) (LoginResp, error) {
|
|
|
|
|
var result LoginResp
|
|
|
|
|
if len(body) == 0 {
|
|
|
|
|
return result, fmt.Errorf("登录响应为空")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
matchs := wk.LoginRegexp.FindStringSubmatch(string(body))
|
|
|
|
|
if len(matchs) > 1 {
|
|
|
|
|
if err := json.Unmarshal([]byte(strings.TrimSpace(matchs[1])), &result); err != nil {
|
|
|
|
|
return result, fmt.Errorf("解析登录响应失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := json.Unmarshal(body, &result); err != nil {
|
|
|
|
|
return result, fmt.Errorf("解析登录响应失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loginErrorMessage(result LoginResp) string {
|
|
|
|
|
if msg := strings.TrimSpace(result.FormError.Code); msg != "" {
|
|
|
|
|
return msg
|
|
|
|
|
}
|
|
|
|
|
if msg := strings.TrimSpace(result.Msg); msg != "" {
|
|
|
|
|
return msg
|
|
|
|
|
}
|
|
|
|
|
return "未知错误"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// CourseParse: 课程解析
|
|
|
|
|
func (wk *WK) CourseParse(content string) ([]Course, error) {
|
|
|
|
|
courses := make([]Course, 0)
|
|
|
|
|
@@ -237,8 +301,7 @@ func (wk *WK) CourseParse(content string) ([]Course, error) {
|
|
|
|
|
// CourseGet: 课程获取
|
|
|
|
|
func (wk *WK) CourseGet(kind CourseKind) ([]Course, error) {
|
|
|
|
|
var courses []Course
|
|
|
|
|
resp, err := wk.Req.
|
|
|
|
|
R().
|
|
|
|
|
resp, err := wk.newRequest().
|
|
|
|
|
SetQueryParam("kind", string(kind)).
|
|
|
|
|
Get(fmt.Sprintf("https://%s/user/index", wk.Host))
|
|
|
|
|
if err != nil {
|
|
|
|
|
@@ -292,8 +355,7 @@ func (wk *WK) UserInfoParse(content string) (User, error) {
|
|
|
|
|
// UserGet: 用户信息获取
|
|
|
|
|
func (wk *WK) UserInfoGet() (User, error) {
|
|
|
|
|
var user User
|
|
|
|
|
resp, err := wk.Req.
|
|
|
|
|
R().
|
|
|
|
|
resp, err := wk.newRequest().
|
|
|
|
|
Get(fmt.Sprintf("https://%s/user/member", wk.Host))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return user, fmt.Errorf("获取用户信息页面失败: %w", err)
|
|
|
|
|
@@ -309,9 +371,8 @@ func (wk *WK) UserInfoGet() (User, error) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Online: 保持账号状态
|
|
|
|
|
func (wk *WK) Online() (bool, error) {
|
|
|
|
|
resp, err := wk.Req.
|
|
|
|
|
R().
|
|
|
|
|
func (wk *WK) performOnline() (bool, error) {
|
|
|
|
|
resp, err := wk.newRequest().
|
|
|
|
|
SetHeaders(map[string]string{
|
|
|
|
|
"x-requested-with": "XMLHttpRequest",
|
|
|
|
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
|
|
|
@@ -321,12 +382,19 @@ func (wk *WK) Online() (bool, error) {
|
|
|
|
|
if err != nil {
|
|
|
|
|
return false, fmt.Errorf("保持账号状态失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if isLoginTimeoutBody(resp.Bytes()) {
|
|
|
|
|
return false, fmt.Errorf("保持账号状态失败: %w", ErrLoginTimeout)
|
|
|
|
|
}
|
|
|
|
|
log.Info("保持账号状态", zap.Any("resp", string(resp.Bytes())))
|
|
|
|
|
return true, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) Online() (bool, error) {
|
|
|
|
|
return withAutoRelogin(wk, "保持账号状态", wk.performOnline)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Study: 学习
|
|
|
|
|
func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
|
|
|
|
|
func (wk *WK) performStudy(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
|
|
|
|
|
var data map[string]string
|
|
|
|
|
switch status {
|
|
|
|
|
case StudyStart:
|
|
|
|
|
@@ -363,8 +431,7 @@ func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*Stu
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("传入的学习状态不匹配")
|
|
|
|
|
}
|
|
|
|
|
resp, err := wk.Req.
|
|
|
|
|
R().
|
|
|
|
|
resp, err := wk.newRequest().
|
|
|
|
|
SetHeaders(map[string]string{
|
|
|
|
|
"x-requested-with": "XMLHttpRequest",
|
|
|
|
|
"Accept": "application/json, text/javascript, */*; q=0.01",
|
|
|
|
|
@@ -376,6 +443,9 @@ func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*Stu
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("进行学习失败: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if isLoginTimeoutBody(resp.Bytes()) {
|
|
|
|
|
return nil, fmt.Errorf("进行学习失败: %w", ErrLoginTimeout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result StudyResp
|
|
|
|
|
if err := json.Unmarshal(resp.Bytes(), &result); err != nil {
|
|
|
|
|
@@ -386,6 +456,12 @@ func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*Stu
|
|
|
|
|
return &result, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) Study(nodeID, studyID, studyTime string, status StudyStatus) (*StudyResp, error) {
|
|
|
|
|
return withAutoRelogin(wk, "进行学习", func() (*StudyResp, error) {
|
|
|
|
|
return wk.performStudy(nodeID, studyID, studyTime, status)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetStudyList: 获取学习记录
|
|
|
|
|
func (wk *WK) GetStudyList(courseID, page string) (*AllRecordResp[StudyList], error) {
|
|
|
|
|
return GetRecords[StudyList](wk, RecordStudy, courseID, page)
|
|
|
|
|
@@ -407,3 +483,101 @@ func (wk *WK) GetExamList(courseID, page string) (*AllRecordResp[ExamList], erro
|
|
|
|
|
func (wk *WK) GetDiscussList(courseID, page string) (*AllRecordResp[ExamList], error) {
|
|
|
|
|
return GetRecords[ExamList](wk, RecordStudy, courseID, page)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isLoginTimeoutBody(body []byte) bool {
|
|
|
|
|
if len(body) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var payload struct {
|
|
|
|
|
Offline int `json:"offline"`
|
|
|
|
|
Status bool `json:"status"`
|
|
|
|
|
Msg string `json:"msg"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return payload.Offline == 1 && !payload.Status && strings.Contains(payload.Msg, "登录超时")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) reloginForExpiredSession(action string, attempt int) error {
|
|
|
|
|
wk.authMu.Lock()
|
|
|
|
|
defer wk.authMu.Unlock()
|
|
|
|
|
|
|
|
|
|
if strings.TrimSpace(wk.Username) == "" || strings.TrimSpace(wk.Password) == "" {
|
|
|
|
|
return fmt.Errorf("%w: 检测到 %s 登录超时", ErrReloginSkipped, action)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Warn("检测到登录超时,开始自动重新登录",
|
|
|
|
|
zap.String("action", action),
|
|
|
|
|
zap.String("host", wk.Host),
|
|
|
|
|
zap.String("username", wk.Username),
|
|
|
|
|
zap.Int("attempt", attempt),
|
|
|
|
|
zap.Int("maxAttempts", maxAutoReloginRetries),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ok, err := wk.Login()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("第 %d 次自动重新登录失败: %w", attempt, err)
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("第 %d 次自动重新登录失败", attempt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Info("自动重新登录成功",
|
|
|
|
|
zap.String("action", action),
|
|
|
|
|
zap.String("host", wk.Host),
|
|
|
|
|
zap.String("username", wk.Username),
|
|
|
|
|
zap.Int("attempt", attempt),
|
|
|
|
|
)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (wk *WK) removeBoundSession(action string, cause error) {
|
|
|
|
|
if wk == nil || wk.sessionManager == nil || wk.sessionID == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
log.Warn("自动重新登录失败,删除失效会话",
|
|
|
|
|
zap.String("action", action),
|
|
|
|
|
zap.String("session_id", wk.sessionID),
|
|
|
|
|
zap.String("host", wk.Host),
|
|
|
|
|
zap.String("username", wk.Username),
|
|
|
|
|
zap.Error(cause),
|
|
|
|
|
)
|
|
|
|
|
wk.sessionManager.Del(wk.sessionID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func withAutoRelogin[T any](wk *WK, action string, fn func() (T, error)) (T, error) {
|
|
|
|
|
result, err := fn()
|
|
|
|
|
if !errors.Is(err, ErrLoginTimeout) {
|
|
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastErr := err
|
|
|
|
|
for attempt := 1; attempt <= maxAutoReloginRetries; attempt++ {
|
|
|
|
|
reloginErr := wk.reloginForExpiredSession(action, attempt)
|
|
|
|
|
if reloginErr != nil {
|
|
|
|
|
lastErr = reloginErr
|
|
|
|
|
if errors.Is(reloginErr, ErrReloginSkipped) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
result, err = fn()
|
|
|
|
|
if !errors.Is(err, ErrLoginTimeout) {
|
|
|
|
|
return result, err
|
|
|
|
|
}
|
|
|
|
|
lastErr = err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
wk.removeBoundSession(action, lastErr)
|
|
|
|
|
|
|
|
|
|
var zero T
|
|
|
|
|
if lastErr == nil {
|
|
|
|
|
lastErr = ErrLoginTimeout
|
|
|
|
|
}
|
|
|
|
|
return zero, fmt.Errorf("%w: %s 自动重新登录 %d 次后仍未恢复: %v", ErrSessionRemoved, action, maxAutoReloginRetries, lastErr)
|
|
|
|
|
}
|
|
|
|
|
|