- 提取 getWKFromContext 辅助函数,消除 handler 中 5 处重复代码 - 提取 retryCode 函数,消除 Login/performStudy 中验证码重试重复 - 提取 removeSession 内部方法,消除 Del/ClearAll/ClearExpired 中 3 处重复 - 提取 WK.UserKey() 方法,消除 4 处 userKey 手动拼接 - SessionManager.Get() 改用 RLock 优化读性能 - GetRecords 递归分页改为迭代,避免栈溢出 - prepareRequestClient 添加配置缓存,仅在 debug 设置变化时重建 - 修正 schedule.go 时区为 Asia/Shanghai + cron "0 6 * * *" - 修正 typo "以达到" → "已达到" - 删除未使用的 QAList struct - 修复 bufferHub.append 切片内存泄漏 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
620 lines
16 KiB
Go
620 lines
16 KiB
Go
package ckwk
|
|
|
|
import (
|
|
"ckwk/internal/conf"
|
|
"ckwk/pkg/common"
|
|
"ckwk/pkg/log"
|
|
"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"`
|
|
Host string `json:"host"`
|
|
Req *resty.Client
|
|
|
|
Cookies []*http.Cookie
|
|
|
|
LoginRegexp *regexp.Regexp
|
|
CourseIDRegexp *regexp.Regexp
|
|
TimeRegexp *regexp.Regexp
|
|
|
|
authMu sync.Mutex
|
|
sessionID string
|
|
sessionManager *SessionManager
|
|
|
|
// 缓存上次应用到的 debug 配置,仅在配置变化时重建
|
|
lastDebugEnabled bool
|
|
lastDebugProxy string
|
|
lastDebugSkipSSLVerify bool
|
|
}
|
|
|
|
func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
|
hasAuth := (username != "" && password != "") || len(cookies) > 0
|
|
if host == "" || !hasAuth {
|
|
return nil
|
|
}
|
|
|
|
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.IsRuntimeDebugEnabled(),
|
|
}
|
|
if conf.IsRuntimeDebugEnabled() {
|
|
reqCfg.Proxy = conf.DebugProxy
|
|
reqCfg.VerifySSL = !conf.DebugSkipSSLVerify
|
|
}
|
|
|
|
req := request.NewClient(reqCfg)
|
|
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}`),
|
|
}
|
|
|
|
return wk
|
|
}
|
|
|
|
func (wk *WK) bindSession(sm *SessionManager, sessionID string) {
|
|
wk.sessionManager = sm
|
|
wk.sessionID = sessionID
|
|
}
|
|
|
|
// UserKey 返回 "host:username" 格式的用户标识,用于 SessionManager 的 userToSession 索引
|
|
func (wk *WK) UserKey() string {
|
|
return wk.Host + ":" + wk.Username
|
|
}
|
|
|
|
func (wk *WK) prepareRequestClient() {
|
|
if wk == nil || wk.Req == nil {
|
|
return
|
|
}
|
|
|
|
debugEnabled := conf.IsRuntimeDebugEnabled()
|
|
debugProxy := conf.DebugProxy
|
|
debugSkipSSLVerify := conf.DebugSkipSSLVerify
|
|
|
|
// 仅在 debug 配置变化时重建
|
|
if wk.lastDebugEnabled == debugEnabled &&
|
|
wk.lastDebugProxy == debugProxy &&
|
|
wk.lastDebugSkipSSLVerify == debugSkipSSLVerify {
|
|
return
|
|
}
|
|
|
|
cfg := &request.Config{
|
|
UserAgent: request.DefaultUserAgent,
|
|
VerifySSL: true,
|
|
Debug: debugEnabled,
|
|
}
|
|
if debugEnabled {
|
|
cfg.Proxy = debugProxy
|
|
cfg.VerifySSL = !debugSkipSSLVerify
|
|
}
|
|
|
|
request.ApplyConfig(wk.Req, cfg)
|
|
if len(wk.Cookies) > 0 {
|
|
wk.Req.SetCookies(wk.Cookies)
|
|
}
|
|
|
|
wk.lastDebugEnabled = debugEnabled
|
|
wk.lastDebugProxy = debugProxy
|
|
wk.lastDebugSkipSSLVerify = debugSkipSSLVerify
|
|
}
|
|
|
|
func (wk *WK) newRequest() *resty.Request {
|
|
wk.prepareRequestClient()
|
|
return wk.Req.R()
|
|
}
|
|
|
|
// 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.newRequest().
|
|
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.newRequest().
|
|
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
|
|
}
|
|
|
|
// retryCode 重试获取验证码,最多 maxRetries 次
|
|
func retryCode(wk *WK, maxRetries int) (string, error) {
|
|
for i := 1; i <= maxRetries; i++ {
|
|
yzm, _ := wk.Code()
|
|
if yzm != "" {
|
|
return yzm, nil
|
|
}
|
|
log.Warnf("第 %d 次获取验证码失败, 正在重试...\n", i)
|
|
}
|
|
return "", fmt.Errorf("已达到最大重试次数,验证码获取失败")
|
|
}
|
|
|
|
// Login: Login WebSite
|
|
func (wk *WK) Login() (bool, error) {
|
|
yzm, err := retryCode(wk, 3)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
resp, err := wk.newRequest().
|
|
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)
|
|
}
|
|
result, err := wk.parseLoginResp(resp.Bytes())
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !result.Status {
|
|
return false, fmt.Errorf("登录失败: %s", loginErrorMessage(result))
|
|
}
|
|
wk.SetCookies(resp.Cookies())
|
|
log.Info("登录成功", zap.Any("cookies", wk.Req.Cookies()))
|
|
return true, nil
|
|
}
|
|
|
|
// parseLoginResp: 解析登录响应
|
|
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)
|
|
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`)
|
|
if creditNode != nil {
|
|
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.newRequest().
|
|
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
|
|
}
|
|
|
|
// QuestionAnswerParse: 解析题目答案
|
|
func (wk *WK) QuestionAnswerParse(content string) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
// UserGet: 用户信息获取
|
|
func (wk *WK) UserInfoGet() (User, error) {
|
|
var user User
|
|
resp, err := wk.newRequest().
|
|
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) performOnline() (bool, error) {
|
|
resp, err := wk.newRequest().
|
|
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)
|
|
}
|
|
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) performStudy(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.newRequest().
|
|
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)
|
|
}
|
|
if isLoginTimeoutBody(resp.Bytes()) {
|
|
return nil, fmt.Errorf("进行学习失败: %w", ErrLoginTimeout)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// GetWorkList: 获取作业记录
|
|
func (wk *WK) GetWorkList(courseID, page string) (*AllRecordResp[WorkList], error) {
|
|
return GetRecords[WorkList](wk, RecordWork, courseID, page)
|
|
}
|
|
|
|
// GetExamList: 获取考试记录
|
|
func (wk *WK) GetExamList(courseID, page string) (*AllRecordResp[ExamList], error) {
|
|
return GetRecords[ExamList](wk, RecordExam, courseID, page)
|
|
}
|
|
|
|
// GetDiscussList: 获取讨论记录
|
|
func (wk *WK) GetDiscussList(courseID, page string) (*AllRecordResp[StudyList], error) {
|
|
return GetRecords[StudyList](wk, RecordDiscuss, 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)
|
|
}
|