✨ 学习通
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
package httpclient
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/utils"
|
||||
"time"
|
||||
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
var Xuexitong = resty.New().
|
||||
SetCookieJar(cookiejar.NewPersistCookieJar()).
|
||||
SetTimeout(30 * time.Second).
|
||||
SetTLSClientConfig(&tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
})
|
||||
|
||||
func init() {
|
||||
if !utils.IsDebug() {
|
||||
Xuexitong.SetDebug(false)
|
||||
} else {
|
||||
Xuexitong.SetProxy("http://127.0.0.1:9000")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"encoding/base64"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
KEY = "u2oh6Vu^HWe4_AES"
|
||||
)
|
||||
|
||||
func LoginEncrypt(str string) (string, error) {
|
||||
key := []byte(KEY)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
logger.Logger.Error("aes.NewCipher err:", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
paddedData := pad([]byte(str), block.BlockSize())
|
||||
ciphertext := make([]byte, len(paddedData))
|
||||
mode := cipher.NewCBCEncrypter(block, key)
|
||||
mode.CryptBlocks(ciphertext, paddedData)
|
||||
data := base64.StdEncoding.EncodeToString(ciphertext)
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// pad 确保数据长度是块大小的整数倍,以便符合块加密算法的要求
|
||||
func pad(src []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(src)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(src, padText...)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package entity
|
||||
|
||||
// XueXiTCourse 课程所有信息
|
||||
type XueXiTCourseJson struct {
|
||||
Result int `json:"result"`
|
||||
Msg string `json:"msg"`
|
||||
ChannelList []ChannelItem `json:"channelList"`
|
||||
Mcode string `json:"mcode"`
|
||||
Createcourse int `json:"createcoursed"`
|
||||
TeacherEndCourse int `json:"teacherEndCourse"`
|
||||
ShowEndCourse int `json:"showEndCourse"`
|
||||
HasMore bool `json:"hasMore"`
|
||||
StuEndCourse int `json:"stuEndCourse"`
|
||||
}
|
||||
|
||||
// ChannelItem 课程列表
|
||||
type ChannelItem struct {
|
||||
Cfid int `json:"cfid"`
|
||||
Norder int `json:"norder"`
|
||||
CataName string `json:"cataName"`
|
||||
Cataid string `json:"cataid"`
|
||||
Id int `json:"id"`
|
||||
Cpi int `json:"cpi"`
|
||||
Key any `json:"key"`
|
||||
Content struct {
|
||||
Studentcount int `json:"studentcount"`
|
||||
Chatid string `json:"chatid"`
|
||||
IsFiled int `json:"isFiled"`
|
||||
Isthirdaq int `json:"isthirdaq"`
|
||||
Isstart bool `json:"isstart"`
|
||||
Isretire int `json:"isretire"`
|
||||
Name string `json:"name"`
|
||||
Course struct {
|
||||
Data []struct {
|
||||
BelongSchoolId string `json:"belongSchoolId"`
|
||||
Coursestate int `json:"coursestate"`
|
||||
Teacherfactor string `json:"teacherfactor"`
|
||||
IsCourseSquare int `json:"isCourseSquare"`
|
||||
Schools string `json:"schools"`
|
||||
CourseSquareUrl string `json:"courseSquareUrl"`
|
||||
Imageurl string `json:"imageurl"`
|
||||
AppInfo string `json:"appInfo"`
|
||||
Name string `json:"name"`
|
||||
DefaultShowCatalog int `json:"defaultShowCatalog"`
|
||||
Id int `json:"id"`
|
||||
AppData int `json:"appData"`
|
||||
} `json:"data"`
|
||||
} `json:"course"`
|
||||
Roletype int `json:"roletype"`
|
||||
Id int `json:"id"`
|
||||
State int `json:"state"`
|
||||
Cpi int `json:"cpi"`
|
||||
Bbsid string `json:"bbsid"`
|
||||
IsSquare int `json:"isSquare"`
|
||||
} `json:"content"`
|
||||
Topsign int `json:"topsign"`
|
||||
}
|
||||
|
||||
type XueXiTCourse struct {
|
||||
Cpi int `json:"cpi"` // 用户唯一标识
|
||||
Key string `json:"key"` // classID 在课程API中为key
|
||||
CourseID string `json:"courseId"` // 课程ID
|
||||
ChatID string `json:"chatId"`
|
||||
CourseTeacher string `json:"courseTeacher"` // 课程老师
|
||||
CourseName string `json:"courseName"` //课程名
|
||||
CourseImage string `json:"courseImage"`
|
||||
// 两个标识 暂时不知道有什么用
|
||||
CourseDataID int `json:"courseDataId"`
|
||||
ContentID int `json:"ContentID"`
|
||||
IsStart bool `json:"isstart"` //是否开课了,开课了为true,没开课为false。一般来说开课才能刷,不然刷不了的
|
||||
State int `json:"state"` //课程状态,0为正常,1为课程已经结束
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/xuexitong/entity"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"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识别
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/application/cookiejar"
|
||||
"likedream-goframe/internal/application/httpclient"
|
||||
"likedream-goframe/internal/application/xuexitong/cryptoutil"
|
||||
"likedream-goframe/internal/framework/logger"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"resty.dev/v3"
|
||||
)
|
||||
|
||||
const (
|
||||
ApiLoginWeb = "https://passport2.chaoxing.com/fanyalogin"
|
||||
)
|
||||
|
||||
type XuexitongLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func NewXuexitongLogin(ctx context.Context) *XuexitongLogin {
|
||||
return &XuexitongLogin{
|
||||
client: httpclient.Xuexitong.Clone(ctx),
|
||||
}
|
||||
}
|
||||
func (c *XuexitongLogin) Login(ctx context.Context, username, password string, retry int) (err error) {
|
||||
usernameEncrypted, err := cryptoutil.LoginEncrypt(username)
|
||||
if err != nil {
|
||||
logger.Logger.Error("LoginEncrypt err:", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
passwordEncrypted, err := cryptoutil.LoginEncrypt(password)
|
||||
if err != nil {
|
||||
logger.Logger.Error("LoginEncrypt err:", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
resp, err := c.client.R().SetFormData(map[string]string{
|
||||
"fid": "-1",
|
||||
"uname": usernameEncrypted,
|
||||
"password": passwordEncrypted,
|
||||
"refer": "http%3A%2F%2Fi.mooc.chaoxing.com",
|
||||
"t": "true",
|
||||
"forbidotherlogin": "0",
|
||||
"validate": "",
|
||||
"doubleFactorLogin": "0",
|
||||
"independentId": "0",
|
||||
"independentNameId": "0",
|
||||
}).Post(ApiLoginWeb)
|
||||
if err != nil {
|
||||
logger.Logger.Error("LoginPost err:", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
body := resp.String()
|
||||
//触发风控直接重新登录
|
||||
if strings.Contains(body, "很抱歉,您所浏览的页面暂时不能访问!") && retry > 0 {
|
||||
time.Sleep(time.Duration(retry*5) * time.Second)
|
||||
return c.Login(ctx, username, password, retry-1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *XuexitongLogin) GetCookieJar() http.CookieJar {
|
||||
return c.client.CookieJar()
|
||||
}
|
||||
func (c *XuexitongLogin) GetCookieString() string {
|
||||
cookieJar := c.GetCookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
func (c *XuexitongLogin) GetClient() *resty.Client {
|
||||
return c.client.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"context"
|
||||
"likedream-goframe/internal/utils"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestXuexitongLogin(t *testing.T) {
|
||||
xuexitongLogin := NewXuexitongLogin(context.Background())
|
||||
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
|
||||
utils.OkOrPanic(err)
|
||||
t.Log(xuexitongLogin.GetCookieString())
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package xuexitong
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
APP_VERSION = "6.6.4"
|
||||
DEVICE_VENDOR = "MI10"
|
||||
BUILD = "10831_263"
|
||||
//BUILD = "10903_285"
|
||||
ANDROID_VERSION = "Android 11"
|
||||
)
|
||||
|
||||
var IMEI = TokenHex(16)
|
||||
|
||||
// GetUA 构建并获取 UA
|
||||
func GetUA(uaType string) string {
|
||||
switch uaType {
|
||||
case "mobile":
|
||||
//return strings.Join([]string{
|
||||
// fmt.Sprintf("Dalvik/2.1.0 (Linux; U; %s; %s Build/SKQ1.210216.001)", ANDROID_VERSION, DEVICE_VENDOR),
|
||||
// fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
|
||||
// fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
|
||||
// "Language/zh_CN",
|
||||
// fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", APP_VERSION, BUILD),
|
||||
// //APP_VERSION,
|
||||
// fmt.Sprintf("(@Kalimdor)_%s", IMEI),
|
||||
//}, " ")
|
||||
|
||||
return strings.Join([]string{
|
||||
fmt.Sprintf("Mozilla/5.0 (Linux; %s; %s Build/OPM1.171019.019; wv)", ANDROID_VERSION, DEVICE_VENDOR),
|
||||
fmt.Sprintf("AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36"),
|
||||
fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
|
||||
fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
|
||||
"Language/zh_CN",
|
||||
fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", APP_VERSION, BUILD),
|
||||
fmt.Sprintf("(@Kalimdor)_%s", IMEI),
|
||||
}, " ")
|
||||
case "web":
|
||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.35"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// 用于生成IMEI
|
||||
func TokenHex(len int) string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
token := hex.EncodeToString(b)
|
||||
return token
|
||||
}
|
||||
|
||||
func MobileUASign(model, locale, version, build, imei string) string {
|
||||
// 拼接字符串,和 Python 的 " ".join 相同
|
||||
str := strings.Join([]string{
|
||||
"(schild:ipL$TkeiEmfy1gTXb2XHrdLN0a@7c^vu)",
|
||||
fmt.Sprintf("(device:%s)", model),
|
||||
fmt.Sprintf("Language/%s", locale),
|
||||
fmt.Sprintf("com.chaoxing.mobile/ChaoXingStudy_3_%s_android_phone_%s", version, build),
|
||||
fmt.Sprintf("(@Kalimdor)_%s", imei),
|
||||
}, " ")
|
||||
|
||||
// 计算 md5
|
||||
hash := md5.Sum([]byte(str))
|
||||
|
||||
// 转换为小写 hex 字符串
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
Reference in New Issue
Block a user