✨ 学习通相关

This commit is contained in:
sugarBecomer
2025-11-25 18:59:45 +08:00
parent 77ea55eda2
commit 644ceed436
12 changed files with 385 additions and 258 deletions
+54 -1
View File
@@ -1,3 +1,56 @@
package captcha
func ApiSemiOCRVerification() {}
import (
"encoding/base64"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/samber/oops"
"github.com/tidwall/gjson"
)
func ApiSemiOCRVerification(bytes []byte, shape1, shape2 int64) string {
data := Bytes2Base64(bytes)
ctx := gctx.GetInitCtx()
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/semiOcr"
client := httpclient.Cpdaily.Clone(ctx)
client.SetDoNotParseResponse(false)
post, err := client.R().SetBody(map[string]any{
"base64": data,
"shape1": shape1,
"shape2": shape2,
}).Post(baseUrl)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("SemiOCRVerification", baseUrl).
WithContext(ctx).
Wrap(err)
})
result := post.String()
return gjson.Get(result, "data").String()
}
func ApiAutoOCRVerification(bytes []byte) string {
data := Bytes2Base64(bytes)
ctx := gctx.GetInitCtx()
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/ocr"
client := httpclient.Cpdaily.Clone(ctx)
client.SetDoNotParseResponse(false)
post, err := client.R().SetBody(map[string]any{
"base64": data,
}).Post(baseUrl)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("AutoOCRVerification", baseUrl).
WithContext(ctx).
Wrap(err)
})
result := post.String()
return gjson.Get(result, "data").String()
}
func Bytes2Base64(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
+18 -67
View File
@@ -8,20 +8,21 @@ import (
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"strings"
"sync"
)
type domainMap map[string]cookieMap
// AllSavePersistCookieJar 支持泛域名的 Cookie Jar
// 不同的二级域名会共享相同主域名的 cookies
// 例如: passport2.chaoxing.com 和 mooc1-api.chaoxing.com 共享 .chaoxing.com 的 cookies
// AllSavePersistCookieJar 完全无限制的 Cookie Jar
// 所有域名的 cookies 都保存在一起,访问任何域名都返回所有 cookies
// 不检查域名、路径等限制,适用于需要跨域共享 session 的场景
type AllSavePersistCookieJar struct {
domainMap domainMap
*sync.RWMutex
}
const globalDomain = "*" // 使用全局域名存储所有 cookies
func NewAllSavePersistCookieJar() *AllSavePersistCookieJar {
return &AllSavePersistCookieJar{
domainMap: make(domainMap),
@@ -29,47 +30,22 @@ func NewAllSavePersistCookieJar() *AllSavePersistCookieJar {
}
}
// extractMainDomain 提取主域名
// 例如: passport2.chaoxing.com -> chaoxing.com
//
// mooc1-api.chaoxing.com -> chaoxing.com
// www.example.co.uk -> example.co.uk
func extractMainDomain(host string) string {
// 移除端口号
if colonIndex := strings.Index(host, ":"); colonIndex != -1 {
host = host[:colonIndex]
}
parts := strings.Split(host, ".")
if len(parts) <= 2 {
// 已经是主域名或单级域名
return host
}
// 处理特殊的二级顶级域名,如 .co.uk, .com.cn 等
// 简化处理:取最后两部分
// 更复杂的情况可以使用 publicsuffix 包
return strings.Join(parts[len(parts)-2:], ".")
}
func (a *AllSavePersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
a.Lock()
defer a.Unlock()
// 提取主域名
mainDomain := extractMainDomain(u.Host)
if _, exists := a.domainMap[mainDomain]; !exists {
a.domainMap[mainDomain] = make(cookieMap)
// 使用全局域名,所有 cookies 保存在一起
if _, exists := a.domainMap[globalDomain]; !exists {
a.domainMap[globalDomain] = make(cookieMap)
}
for _, cookie := range cookies {
if a.domainMap[mainDomain] == nil {
a.domainMap[mainDomain] = make(cookieMap)
if a.domainMap[globalDomain] == nil {
a.domainMap[globalDomain] = make(cookieMap)
}
// 查找是否存在相同 name, path 和 domain 的 cookie
existingCookies := a.domainMap[mainDomain][cookie.Name]
// 查找是否存在相同 name 的 cookie(忽略 path 和 domain)
existingCookies := a.domainMap[globalDomain][cookie.Name]
found := false
for i, existing := range existingCookies {
if existing.Path == cookie.Path && existing.Domain == cookie.Domain {
@@ -82,7 +58,7 @@ func (a *AllSavePersistCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie)
if !found {
// 没有找到匹配的,添加新的 cookie
a.domainMap[mainDomain][cookie.Name] = append(a.domainMap[mainDomain][cookie.Name], cookie)
a.domainMap[globalDomain][cookie.Name] = append(a.domainMap[globalDomain][cookie.Name], cookie)
}
}
}
@@ -91,15 +67,9 @@ func (a *AllSavePersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
a.RLock()
defer a.RUnlock()
// 提取主域名
mainDomain := extractMainDomain(u.Host)
if cookiesList, exists := a.domainMap[mainDomain]; exists {
// 直接返回全局域名下的所有 cookies,不检查域名和路径
if cookiesList, exists := a.domainMap[globalDomain]; exists {
result := make([]*http.Cookie, 0, len(cookiesList))
requestPath := u.Path
if requestPath == "" {
requestPath = "/"
}
for _, cookies := range cookiesList {
// 如果只有一个 cookie,直接返回
@@ -108,28 +78,9 @@ func (a *AllSavePersistCookieJar) Cookies(u *url.URL) []*http.Cookie {
continue
}
// 有多个同名 cookie,选择最匹配的(path 最长且匹配请求路径的)
var bestMatch *http.Cookie
bestMatchLen := -1
for _, cookie := range cookies {
cookiePath := cookie.Path
if cookiePath == "" {
cookiePath = "/"
}
// 检查 path 是否匹配
if pathMatch(requestPath, cookiePath) {
if len(cookiePath) > bestMatchLen {
bestMatch = cookie
bestMatchLen = len(cookiePath)
}
}
}
// 如果找到匹配的,添加到结果中
if bestMatch != nil {
result = append(result, bestMatch)
// 有多个同名 cookie,返回最后设置的那个(列表最后一个)
if len(cookies) > 0 {
result = append(result, cookies[len(cookies)-1])
}
}
return result
@@ -13,6 +13,9 @@ func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailI
// 安徽建筑大学
case "ahjzu":
return NewAhjzu(ctx)
// 南宁师范大学
case "2ba0ea59-a764-4482-bb2b-c924b726b04c":
return NewNnnu(ctx)
// 河北石油职业技术大学
case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a":
return NewIap(ctx)
@@ -391,6 +391,10 @@ func handleCasLoginSuccess(ctx context.Context, response *resty.Response, school
utils.OkOrPanic(errors.NewBusinessError(201, "多因子验证码认证").WithData(key))
// 否则应该是登录成功
} else if statusCode == 200 || statusCode == 404 {
all, _ := io.ReadAll(verifyResponse.Body)
if strings.Contains(string(all), "完善资料") || strings.Contains(string(all), "为了你的帐号安全") {
utils.OkOrPanic(errors.NewBusinessError(503, "请手动学工号登录完善信息,修改密码").WithData(schoolInfo.WecloudUrl))
}
ActivateSession(ctx, schoolInfo.WecloudUrl, client)
} else {
utils.OkOrPanic(errors.NewBusinessError(503, "未知错误导致今日校园登录失败"),
+149
View File
@@ -0,0 +1,149 @@
package login
import (
"bytes"
"context"
"fmt"
"io"
"likedream-goframe/internal/application/cookiejar"
"likedream-goframe/internal/application/cpdaily/cryptoutil"
"likedream-goframe/internal/application/cpdaily/school"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/errors"
"likedream-goframe/internal/utils"
"net/http"
"net/url"
"github.com/PuerkitoBio/goquery"
"github.com/samber/oops"
"resty.dev/v3"
)
// 南宁师范大学登录实现
type CpdailyNnnuLogin struct {
client *resty.Client
}
func (c CpdailyNnnuLogin) Login(ctx context.Context, schoolDetail *school.SchoolDetailInfo, username, password string) {
jar := cookiejar.NewPersistCookieJar()
c.client.SetCookieJar(jar).SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
// 获取合法的登录URL
loginUrl := getCasValidLoginUrl(schoolDetail)
// 请求登录页面并获取真正的登录URL
loginPageResponse, err := c.client.R().
WithContext(ctx).
Get(loginUrl)
defer loginPageResponse.Body.Close()
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
utils.OkOrPanic(err)
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
// 关闭client的重定向
c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
// 解析登录页面
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
utils.OkOrPanic(err)
// 判断登录类型
var (
allInputs = map[string]string{}
form = loginPage.Find("form").First()
)
inputs := form.Find("input")
for _, input := range inputs.EachIter() {
name, exists := input.Attr("name")
if !exists {
continue
}
value, _ := input.Attr("value")
allInputs[name] = value
}
allInputs["username"] = username
// 处理密码
allInputs["password"] = cryptoutil.MD5(password)
// 解决常规图片验证码
captchaUrl := getAhjzuCaptchaUrl(loginResponseUrl)
captchaResult := ResolveOcrCaptcha(ctx, captchaUrl, c.client)
allInputs["validateCode"] = captchaResult
// 登录提交
loginPostResponse, err := c.client.R().
SetFormData(allInputs).
WithContext(ctx).
Post(loginResponseUrl.String())
utils.OkOrPanic(err, func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
Wrap(err)
})
defer loginPostResponse.Body.Close()
switch loginPostResponse.StatusCode() {
case 302:
handleCasLoginSuccess(ctx, loginPostResponse, schoolDetail, username, password, c.client)
case 200:
fallthrough
case 401:
doc, err := goquery.NewDocumentFromReader(loginPostResponse.Body)
utils.OkOrPanic(err, func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
Wrap(err)
})
if isCasNeedKickOut(doc) {
kickoutResponse := submitCasKickOut(ctx, loginResponseUrl.String(), doc, c.client)
handleCasLoginSuccess(ctx, kickoutResponse, schoolDetail, username, password, c.client)
return
}
message := getCasErrorMessage(doc)
utils.OkOrPanic(errors.NewBusinessError(503, fmt.Sprintf("今日校园登录失败: %s", message)), func(err error) error {
return oops.
WithContext(ctx).
With("loginResponseUrl", loginResponseUrl).
With("allInputs", allInputs).
With("errorMessage", message).
Wrap(errors.NewBusinessError(503, message))
})
}
}
func getNuuuCaptchaUrl(url *url.URL) string {
return fmt.Sprintf("%s://%s/sso/code/code.jsp", url.Scheme, url.Host)
}
func (c CpdailyNnnuLogin) GetCookieJar() http.CookieJar {
return c.client.CookieJar()
}
func (c CpdailyNnnuLogin) GetCookieString() string {
cookieJar := c.GetCookieJar()
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
return serializableJar.Serialize()
}
func (c CpdailyNnnuLogin) GetClient() *resty.Client {
return c.client
}
func NewNnnu(ctx context.Context) CpdailyLogin {
return &CpdailyNnnuLogin{
client: httpclient.NewCpdailyClient(),
}
}
+2 -49
View File
@@ -1,56 +1,18 @@
package login
import (
"bytes"
"context"
"encoding/base64"
"image"
"io"
"likedream-goframe/internal/application/httpclient"
"likedream-goframe/internal/common/cst"
"likedream-goframe/internal/application/captcha"
"likedream-goframe/internal/utils"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/samber/oops"
"github.com/tidwall/gjson"
"resty.dev/v3"
)
func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) string {
byteData := DownCode2Byte(ctx, url, client)
//img, err := BytesToImage(byteData)
//utils.OkOrPanic(err, func(err error) error {
// return oops.
// With("BytesToImage", url).
// WithContext(ctx).
// Wrap(err)
//})
//data := ddddocr.AutoOCRVerification(img)
//return data
return ApiAutoOCRVerification(Bytes2Base64(byteData))
}
func Bytes2Base64(data []byte) string {
return base64.StdEncoding.EncodeToString(data)
}
func ApiAutoOCRVerification(data string) string {
ctx := gctx.GetInitCtx()
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/ocr"
client := httpclient.Cpdaily.Clone(ctx)
client.SetDoNotParseResponse(false)
post, err := client.R().SetBody(map[string]any{
"base64": data,
}).Post(baseUrl)
utils.OkOrPanic(err, func(err error) error {
return oops.
With("AutoOCRVerification", baseUrl).
WithContext(ctx).
Wrap(err)
})
result := post.String()
return gjson.Get(result, "data").String()
return captcha.ApiAutoOCRVerification(byteData)
}
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
@@ -73,12 +35,3 @@ func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte
})
return all
}
// 通用解码函数,自动检测格式
func BytesToImage(imgBytes []byte) (image.Image, error) {
img, _, err := image.Decode(bytes.NewReader(imgBytes))
if err != nil {
return nil, err
}
return img, nil
}
+1 -1
View File
@@ -31,7 +31,7 @@ func NewXuexitongClient() *resty.Client {
client := resty.New().
SetCookieJar(nil).
SetTimeout(30*time.Second).
SetDoNotParseResponse(true).
SetDoNotParseResponse(false).
SetTLSClientConfig(&tls.Config{
InsecureSkipVerify: true,
}).
@@ -70,3 +70,6 @@ type XueXiTCourse struct {
IsStart bool `json:"isstart"` //是否开课了,开课了为true,没开课为false。一般来说开课才能刷,不然刷不了的
State int `json:"state"` //课程状态,0为正常,1为课程已经结束
}
type XueXiTCourseChapter struct {
}
@@ -28,6 +28,8 @@ func NewXuexitongLogin(ctx context.Context) *XuexitongLogin {
}
}
func (c *XuexitongLogin) Login(ctx context.Context, username, password string, retry int) (err error) {
jar := cookiejar.NewAllSavePersistCookieJar()
c.client.SetCookieJar(jar)
usernameEncrypted, err := cryptoutil.LoginEncrypt(username)
if err != nil {
logger.Logger.Error("LoginEncrypt err:", zap.Error(err))
@@ -3,10 +3,6 @@ package xuexitong
import (
"fmt"
"io"
"likedream-goframe/internal/utils"
"log"
"math/rand"
"os"
"strings"
"time"
@@ -15,10 +11,8 @@ import (
var randChar []string = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"}
func ApiVerifyCode(client *resty.Client, retry int) (string, error) {
if retry < 0 {
return "", fmt.Errorf("retry limit exceeded")
}
func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
urlStr := "https://mooc1-api.chaoxing.com/processVerifyPng.ac?t=" + fmt.Sprintf("%d", time.Now().UnixMilli())
req := client.R()
@@ -28,40 +22,14 @@ func ApiVerifyCode(client *resty.Client, retry int) (string, error) {
req.Header.Add("Connection", "keep-alive")
res, err := req.Get(urlStr)
if err != nil {
if retry > 0 {
return ApiVerifyCode(client, retry-1)
} else {
return res.String(), err
}
return nil, err
}
codeFileName := "code" + randChar[rand.Intn(len(randChar))] //生成验证码文件名称
for i := 0; i < 10; i++ {
codeFileName += randChar[rand.Intn(len(randChar))]
}
codeFileName += ".png"
utils.PathExistForCreate("./assets/code/") //检测是否存在路径,如果不存在则创建
filepath := fmt.Sprintf("./assets/code/%s", codeFileName)
file, err := os.Create(filepath)
defer res.Body.Close()
all, err := io.ReadAll(res.Body)
if err != nil {
res.Body.Close() //立即释放
log.Println(err)
return "", err
return nil, err
}
_, err = io.Copy(file, res.Body)
if err != nil {
res.Body.Close() //立即释放
log.Println(err)
return "", err
}
file.Close()
if utils.IsBadImg(filepath) {
res.Body.Close() //立即释放
utils.DeleteFile(filepath) //删除坏的文件
return ApiVerifyCode(client, retry-1)
}
return filepath, nil
return all, nil
}
func ApiPassVerifyCode(client *resty.Client, code string, retry int) bool {
+141 -100
View File
@@ -1,102 +1,143 @@
package xuexitong
//
//import (
// "context"
// "errors"
// "likedream-goframe/internal/application/recognizer/ddddocr"
// "likedream-goframe/internal/application/xuexitong/entity"
// "likedream-goframe/internal/framework/logger"
// "likedream-goframe/internal/utils"
// "strings"
//
// "github.com/bytedance/sonic"
// ort "github.com/yalue/onnxruntime_go"
// "resty.dev/v3"
//)
//
//const (
// ApiPullCourses = "https://mooc1-api.chaoxing.com/mycourse/backclazzdata"
//)
//
//type XuexitongCourse struct {
// client *resty.Client
//}
//
//func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
// return &XuexitongCourse{
// client: client,
// }
//}
//
//// PullCourseList 拉取学习通所有课程列表并返回
//func (c *XuexitongCourse) PullCourseList(ctx context.Context, retry int) (courseList []entity.XueXiTCourse, err error) {
// req := c.client.R()
// req.Header.Add("User-Agent", GetUA("mobile"))
// get, err := req.Get(ApiPullCourses)
// if err != nil {
// return nil, err
// }
// body := get.String()
// if strings.Contains(body, "很抱歉,您所浏览的页面不存在") && retry > 0 { //防止学习通抽风
// return c.PullCourseList(ctx, retry-1)
// }
// if strings.Contains(body, "请输入验证码") {
// logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
// //todo ddddocr识别
// succ := false
// for i := 0; i < 5; i++ {
// codePath, err := ApiVerifyCode(c.client, 5)
// if err != nil {
// return nil, err
// }
// if codePath == "" { //如果path为空,那么可能是账号问题
// return nil, errors.New("无法正常获取学习通验证码,请检查对应url是否正常")
// }
// img, _ := utils.ReadImg(codePath) //读取验证码图片
// codeResult := ddddocr.SemiOCRVerification(img, ort.NewShape(1, 23))
// utils.DeleteFile(codePath) //删除验证码文件、
// if ApiPassVerifyCode(c.client, codeResult, 5) {
// succ = true
// break
// }
// }
// if !succ {
// return nil, errors.New("AI验证码识别失败,请检查对应url是否正常")
// }
// }
// var courseResult entity.XueXiTCourseJson
// err = sonic.Unmarshal([]byte(body), &courseResult)
// if err != nil {
// return nil, err
// }
// for _, channel := range courseResult.ChannelList {
// if channel.Content.Course.Data == nil {
// continue
// }
// for _, v := range channel.Content.Course.Data {
// flag := false
// course := entity.XueXiTCourse{
// Cpi: channel.Cpi,
// Key: strings.Split(strings.Split(v.CourseSquareUrl, "classId=")[1], "&userId")[0],
// CourseID: strings.Split(strings.Split(v.CourseSquareUrl, "courseId=")[1], "&personId")[0],
// CourseTeacher: v.Teacherfactor,
// CourseImage: v.Name,
// CourseDataID: v.Id,
// ContentID: channel.Content.Id,
// IsStart: channel.Content.Isstart,
// State: channel.Content.State,
// }
// for _, subCourse := range courseList {
// if subCourse.CourseID == course.CourseID {
// flag = true
// break
// }
// }
// if !flag {
// courseList = append(courseList, course)
// }
// }
// }
// return courseList, nil
//}
import (
"context"
"likedream-goframe/internal/application/captcha"
"likedream-goframe/internal/application/xuexitong/entity"
errors2 "likedream-goframe/internal/common/errors"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"strconv"
"strings"
"time"
"github.com/avast/retry-go/v4"
"github.com/bytedance/sonic"
"github.com/samber/oops"
"resty.dev/v3"
)
const (
ApiPullCourses = "https://mooc1-api.chaoxing.com/mycourse/backclazzdata"
ApiPullChapter = "https://mooc1-api.chaoxing.com/gas/clazz"
)
type XuexitongCourse struct {
client *resty.Client
}
func NewXuexitongCourse(client *resty.Client) *XuexitongCourse {
return &XuexitongCourse{
client: client,
}
}
// PullCourseList 拉取学习通所有课程列表并返回
func (c *XuexitongCourse) PullCourseList(ctx context.Context) []entity.XueXiTCourse {
var body string
err := retry.Do(func() error {
req := c.client.R()
req.Header.Add("User-Agent", GetUA("mobile"))
get, err := req.Get(ApiPullCourses)
if err != nil {
return err
}
body = get.String()
if strings.Contains(body, "很抱歉,您所浏览的页面不存在") { //防止学习通抽风
return errors2.NewBusinessError(503, "课程列表访问失败")
}
if strings.Contains(body, "请输入验证码") {
logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
e := retry.Do(func() error {
bytes, e := ApiVerifyCodeBytes(c.client)
if e != nil {
return e
}
codeResult := captcha.ApiSemiOCRVerification(bytes, 1, 23)
if ApiPassVerifyCode(c.client, codeResult, 5) {
return nil
} else {
return errors2.NewBusinessError(503, "验证码识别失败")
}
}, retry.Attempts(5), retry.Delay(5*time.Second))
return e
}
return nil
}, retry.Attempts(5), retry.Delay(5*time.Second))
if err != nil {
utils.OkOrPanic(errors2.NewBusinessError(503, "拉取课程列表失败").WithInnerError(err), oops.WithContext(ctx).Wrap)
}
var courseResult entity.XueXiTCourseJson
err = sonic.Unmarshal([]byte(body), &courseResult)
if err != nil {
utils.OkOrPanic(errors2.NewBusinessError(503, "解析课程列表失败").WithInnerError(err), oops.WithContext(ctx).Wrap)
}
var courseList []entity.XueXiTCourse
for _, channel := range courseResult.ChannelList {
if channel.Content.Course.Data == nil {
continue
}
for _, v := range channel.Content.Course.Data {
flag := false
course := entity.XueXiTCourse{
Cpi: channel.Cpi,
Key: strings.Split(strings.Split(v.CourseSquareUrl, "classId=")[1], "&userId")[0],
CourseID: strings.Split(strings.Split(v.CourseSquareUrl, "courseId=")[1], "&personId")[0],
ChatID: channel.Content.Chatid,
CourseTeacher: v.Teacherfactor,
CourseName: v.Name,
CourseImage: v.Imageurl,
CourseDataID: v.Id,
ContentID: channel.Content.Id,
IsStart: channel.Content.Isstart,
State: channel.Content.State,
}
for _, subCourse := range courseList {
if subCourse.CourseID == course.CourseID {
flag = true
break
}
}
if !flag {
courseList = append(courseList, course)
}
}
}
return courseList
}
// PullCourseChapterList 拉取学习通课程章节列表并返回
func (c *XuexitongCourse) PullCourseChapterList(ctx context.Context, courseId string) []entity.XueXiTCourseChapter {
courseList := c.PullCourseList(ctx)
var course entity.XueXiTCourse
for _, cour := range courseList {
if cour.CourseID == courseId {
course = cour
break
}
}
var chapterString string
e := retry.Do(func() error {
get, err := c.client.R().SetHeaders(map[string]string{
"User-Agent": GetUA("mobile"),
"Accept-Language": "zh_CN",
"Host": "mooc1-api.chaoxing.com",
"Connection": "Keep-Alive",
}).SetQueryParams(
map[string]string{
"id": course.Key,
"persinid": strconv.Itoa(course.Cpi),
"fields": "id,bbsid,classscore,isstart,allowdownload,chatid,name,state,isfiled,visiblescore,hideclazz,begindate,forbidintoclazz,coursesetting.fields(id,courseid,hiddencoursecover,coursefacecheck),course.fields(id,belongschoolid,name,infocontent,objectid,app,bulletformat,mappingcourseid,imageurl,teacherfactor,jobcount,knowledge.fields(id,name,indexOrder,parentnodeid,status,isReview,layer,label,jobcount,begintime,endtime,attachment.fields(id,type,objectid,extension).type(video)))",
"view": "json",
}).Get(ApiPullChapter)
if err != nil {
return err
}
chapterString = get.String()
return nil
}, retry.Attempts(3), retry.Delay(5*time.Second))
utils.OkOrPanic(e, oops.WithContext(ctx).With("chapterString", chapterString).Wrap)
return nil
}
@@ -18,7 +18,7 @@ func TestPullCourse(t *testing.T) {
utils.OkOrPanic(err)
client := xuexitongLogin.GetClient()
courseAction := NewXuexitongCourse(client)
list, err := courseAction.PullCourseList(context.Background(), 5)
list := courseAction.PullCourseList(context.Background())
utils.OkOrPanic(err)
t.Log(list)
}