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)
|
||||
}
|
||||
31
internal/ckwk/api_test.go
Normal file
31
internal/ckwk/api_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWK(t *testing.T) {
|
||||
// var cookies []*http.Cookie
|
||||
|
||||
// cookie := http.Cookie{
|
||||
// Name: "token",
|
||||
// Value: "sid.odITb8QqlLLPPoQMpnMKZjx98cUGuC",
|
||||
// // Domain: ".leykeji.com",
|
||||
// Path: "/",
|
||||
// }
|
||||
|
||||
// cookies = append(cookies, &cookie)
|
||||
|
||||
// wk := NewWK("2025301201", "Zhilv666", "cqcst.leykeji.com", cookies)
|
||||
|
||||
// wk.Login()
|
||||
|
||||
// user, err := wk.UserInfoGet()
|
||||
// log.Debug("获取用户信息", zap.Any("user", user), zap.Error(err))
|
||||
|
||||
// courses, err := wk.CourseGet(Finish)
|
||||
// log.Debug("获取课程信息", zap.Any("courses", courses), zap.Error(err))
|
||||
fmt.Printf("time.Now().UnixMilli(): %v\n", time.Now().UnixMilli())
|
||||
}
|
||||
111
internal/ckwk/resp.go
Normal file
111
internal/ckwk/resp.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package ckwk
|
||||
|
||||
// 获取验证码响应
|
||||
type CodeResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
// 登录响应
|
||||
type LoginResp struct {
|
||||
RefreshCode int `json:"refresh_code"`
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
Back string `json:"back"`
|
||||
}
|
||||
|
||||
type StudyResp struct {
|
||||
State int `json:"state"`
|
||||
StudyID int `json:"studyId"`
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type AllRecordResp[T any] struct {
|
||||
List []T `json:"list"`
|
||||
PageInfo PageInfo `json:"pageInfo"`
|
||||
Status bool `json:"status"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
KeyName string `json:"keyName"`
|
||||
Page int `json:"page"`
|
||||
PageCount int `json:"pageCount"`
|
||||
RecordsCount int `json:"recordsCount"`
|
||||
OnlyCount int `json:"onlyCount"`
|
||||
PageSize int `json:"pageSize"`
|
||||
}
|
||||
|
||||
// 学习记录列表
|
||||
type StudyList struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type interface{} `json:"type"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
CourseID string `json:"courseId"`
|
||||
VideoFile string `json:"videoFile"`
|
||||
VideoDuration string `json:"videoDuration"`
|
||||
VotingPath interface{} `json:"votingPath"`
|
||||
TabVideo string `json:"tabVideo"`
|
||||
TabFile string `json:"tabFile"`
|
||||
TabVote string `json:"tabVote"`
|
||||
TabWork string `json:"tabWork"`
|
||||
TabExam string `json:"tabExam"`
|
||||
Sort string `json:"sort"`
|
||||
VideoMode string `json:"videoMode"`
|
||||
LocalFile string `json:"localFile"`
|
||||
SchoolID string `json:"schoolId"`
|
||||
Lock string `json:"lock"`
|
||||
UnlockTime string `json:"unlockTime"`
|
||||
Bid string `json:"bid"`
|
||||
Duration string `json:"duration"`
|
||||
Progress string `json:"progress"`
|
||||
State string `json:"state"`
|
||||
ViewCount string `json:"viewCount"`
|
||||
FinalTime string `json:"finalTime"`
|
||||
Error int `json:"error"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
BeginTime string `json:"beginTime"`
|
||||
ViewedDuration string `json:"viewedDuration"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// 考试记录列表
|
||||
type ExamList struct {
|
||||
ID string `json:"id"`
|
||||
UserID interface{} `json:"userId"`
|
||||
Title string `json:"title"`
|
||||
TopicNumber string `json:"topicNumber"`
|
||||
Score string `json:"score"`
|
||||
AddTime string `json:"addTime"`
|
||||
NodeID string `json:"nodeId"`
|
||||
CourseID string `json:"courseId"`
|
||||
LimitedTime string `json:"limitedTime"`
|
||||
Sequence string `json:"sequence"`
|
||||
Remarks string `json:"remarks"`
|
||||
PaperID string `json:"paperId"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
CreateUserID string `json:"createUserId"`
|
||||
ClassList string `json:"classList"`
|
||||
IsPrivate string `json:"isPrivate"`
|
||||
TeacherType string `json:"teacherType"`
|
||||
Allow string `json:"allow"`
|
||||
Frequency string `json:"frequency"`
|
||||
HasCollect string `json:"hasCollect"`
|
||||
SchoolID string `json:"schoolId"`
|
||||
Parsing string `json:"parsing"`
|
||||
AddDate string `json:"addDate"`
|
||||
Random string `json:"random"`
|
||||
RandData interface{} `json:"randData"`
|
||||
RandNumber string `json:"randNumber"`
|
||||
Name string `json:"name"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
State string `json:"state"`
|
||||
SubmitTime string `json:"submitTime"`
|
||||
FinalScore string `json:"finalScore"`
|
||||
FinishTime string `json:"finishTime"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
117
internal/ckwk/session_manager.go
Normal file
117
internal/ckwk/session_manager.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"ckwk/pkg/log"
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type SessionManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]SessionItem
|
||||
userToSession map[string]string
|
||||
}
|
||||
|
||||
type SessionItem struct {
|
||||
Instance *WK
|
||||
LastValue time.Time
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewSessionManager() *SessionManager {
|
||||
return &SessionManager{
|
||||
sessions: make(map[string]SessionItem),
|
||||
userToSession: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// Store: 保存 session 并返回 session id
|
||||
func (m *SessionManager) Store(wk *WK) string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
userKey := wk.Host + ":" + wk.Username
|
||||
if oldID, exists := m.userToSession[userKey]; exists {
|
||||
item := m.sessions[oldID]
|
||||
item.LastValue = time.Now()
|
||||
item.Instance = wk
|
||||
m.sessions[oldID] = item
|
||||
|
||||
log.Info("用户已存在,复用旧 Session",
|
||||
zap.String("id", oldID),
|
||||
zap.String("user", userKey),
|
||||
)
|
||||
|
||||
return oldID
|
||||
}
|
||||
|
||||
sessionID := uuid.New().String()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
m.userToSession[userKey] = sessionID
|
||||
m.sessions[sessionID] = SessionItem{
|
||||
Instance: wk,
|
||||
LastValue: time.Now(),
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
log.Info("创建新 Session", zap.String("id", sessionID))
|
||||
|
||||
go m.KeepAlive(ctx, sessionID, wk)
|
||||
|
||||
return sessionID
|
||||
}
|
||||
|
||||
// Get: 获取指定 session id 的 wk
|
||||
func (m *SessionManager) Get(sessionID string) (*WK, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
item, ok := m.sessions[sessionID]
|
||||
if ok {
|
||||
return item.Instance, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (m *SessionManager) Del(sessionID string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if item, ok := m.sessions[sessionID]; ok {
|
||||
userKey := item.Instance.Host + ":" + item.Instance.Username
|
||||
|
||||
if item.cancel != nil {
|
||||
item.cancel()
|
||||
}
|
||||
|
||||
delete(m.userToSession, userKey)
|
||||
delete(m.sessions, sessionID)
|
||||
|
||||
log.Info("删除 Session", zap.String("id", sessionID))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SessionManager) KeepAlive(ctx context.Context, id string, wk *WK) {
|
||||
ticker := time.NewTicker(2 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
log.Info("启动 KeepAlive", zap.String("id", id))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info("KeepAlive 已停止", zap.String("id", id))
|
||||
return
|
||||
case <-ticker.C:
|
||||
_, err := wk.Online()
|
||||
if err != nil {
|
||||
log.Error("自动保活请求失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
internal/ckwk/types.go
Normal file
77
internal/ckwk/types.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package ckwk
|
||||
|
||||
import (
|
||||
"ckwk/pkg/log"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CourseKind string
|
||||
type RecordType string
|
||||
type StudyStatus int
|
||||
|
||||
const (
|
||||
Run CourseKind = "run" // 我的课程
|
||||
Finish CourseKind = "finish" // 已结束
|
||||
Sign CourseKind = "sign" // 报名中
|
||||
|
||||
RecordStudy RecordType = "" // 学习记录
|
||||
RecordWork RecordType = "/work" // 作业记录
|
||||
RecordExam RecordType = "/exam" // 考试记录
|
||||
RecordDiscuss RecordType = "/discuss" // 讨论记录
|
||||
|
||||
StudyStart StudyStatus = 1 // 开始学习
|
||||
Study StudyStatus = 2 // 学习中
|
||||
StudyOver StudyStatus = 3 // 学习介绍
|
||||
)
|
||||
|
||||
// User: 用户
|
||||
type User struct {
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
Dept string `json:"dept"`
|
||||
Class string `json:"class"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
|
||||
// Course: 课程
|
||||
type Course struct {
|
||||
Name string `json:"name"`
|
||||
ID int `json:"id"`
|
||||
Teacher string `json:"teacher"`
|
||||
Progress string `json:"progress"`
|
||||
StartTime string `json:"start_time"`
|
||||
StopTime string `json:"stop_time"`
|
||||
Credit float32 `json:"credit"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func GetRecords[T any](wk *WK, rType RecordType, courseID, page string) (*AllRecordResp[T], error) {
|
||||
log.Debug("获取记录信息", zap.String("host", wk.Host), zap.String("type", string(rType)))
|
||||
resp, err := wk.Req.
|
||||
R().
|
||||
SetQueryParams(map[string]string{
|
||||
"courseId": courseID,
|
||||
"page": page,
|
||||
"_": fmt.Sprint(time.Now().UnixMilli()),
|
||||
}).
|
||||
Get(fmt.Sprintf("https://%s/user/study_record%s.json", wk.Host, rType))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var result AllRecordResp[T]
|
||||
if err := json.Unmarshal(resp.Bytes(), &result); err != nil {
|
||||
log.Error("JSON解析失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !result.Status {
|
||||
return &result, fmt.Errorf("接口报错: %s", result.Msg)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user