Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8974bd26a5 | |||
| 6f25652183 | |||
| a8f4a2afa6 |
@@ -51,6 +51,24 @@ func ApiAutoOCRVerification(bytes []byte) string {
|
||||
result := post.String()
|
||||
return gjson.Get(result, "data").String()
|
||||
}
|
||||
|
||||
func ApiAutoCalcVerification(b64 string) string {
|
||||
ctx := gctx.GetInitCtx()
|
||||
baseUrl := g.Config().MustGetWithEnv(ctx, cst.ConfigCaptchaBaseUrl).String() + "/calc"
|
||||
client := httpclient.Cpdaily.Clone(ctx)
|
||||
client.SetDoNotParseResponse(false)
|
||||
post, err := client.R().SetBody(map[string]any{
|
||||
"base64": b64,
|
||||
}).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)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ func GetCpdailyLoginBySchoolInfo(ctx context.Context, info *school.SchoolDetailI
|
||||
// 河北石油职业技术大学
|
||||
case "01e270d1-00e2-4ce9-b0af-5d5fbd462b8a":
|
||||
return NewIap(ctx)
|
||||
case "676c9395-a29c-4068-97a8-b48e05dae499":
|
||||
return NewHnuu(ctx)
|
||||
default:
|
||||
if info.IsIap() {
|
||||
return NewIap(ctx)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
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"
|
||||
)
|
||||
|
||||
var RSA_KEY_MAP = map[string]string{
|
||||
"public_exponent": "010001",
|
||||
"private_exponent": "413798867d69babed22e0dd3d4031c635f3e9dbca0fa50a32974a0e230787b7f7ba78caefbee828a051c690357a8cc31dba8efc738b4db22e887571ef1ec5a5a55b6d866f6a67527f6a7d78a127c9f687008bb540228b50aa2d1ca5a4ff71107234f936b611ac46432a26da9c302eaa7180820df70593353b3f8c0247fe97a45",
|
||||
"modulus": "00b5eeb166e069920e80bebd1fea4829d3d1f3216f2aabe79b6c47a3c18dcee5fd22c2e7ac519cab59198ece036dcf289ea8201e2a0b9ded307f8fb704136eaeb670286f5ad44e691005ba9ea5af04ada5367cd724b5a26fdb5120cc95b6431604bd219c6b7d83a6f8f24b43918ea988a76f93c333aa5a20991493d4eb1117e7b1",
|
||||
}
|
||||
|
||||
// 淮南联合大学登录实现 RSA加密 & 验证码必出(加法验证码)
|
||||
type CpdailyHnuuLogin struct {
|
||||
client *resty.Client
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) 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)
|
||||
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
defer loginPageResponse.Body.Close()
|
||||
loginPageBytes, err := io.ReadAll(loginPageResponse.Body)
|
||||
utils.OkOrPanic(err)
|
||||
loginResponseUrl := loginPageResponse.RawResponse.Request.URL
|
||||
// 解析登录页面
|
||||
loginPage, err := goquery.NewDocumentFromReader(bytes.NewReader(loginPageBytes))
|
||||
utils.OkOrPanic(err)
|
||||
// 判断登录类型
|
||||
var (
|
||||
allInputs = map[string]string{}
|
||||
formNode = loginPage.Find(".ant-form").First()
|
||||
)
|
||||
inputs := formNode.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
|
||||
pair := cryptoutil.NewNutRSAKeyPair(RSA_KEY_MAP["public_exponent"], "", RSA_KEY_MAP["modulus"])
|
||||
// 处理密码
|
||||
allInputs["password"] = pair.EncryptedString(password)
|
||||
|
||||
// 解决验证码
|
||||
// 解决常规图片验证码
|
||||
captchaUrl := getHnuuCaptchaUrl(loginResponseUrl)
|
||||
code, uid := ResolveCalcCaptcha(ctx, captchaUrl, c.client)
|
||||
|
||||
allInputs["code"] = code
|
||||
allInputs["id"] = uid
|
||||
jar = cookiejar.NewPersistCookieJar()
|
||||
c.client.SetCookieJar(jar)
|
||||
// 登录提交
|
||||
loginPostResponse, err := c.client.R().
|
||||
SetFormData(allInputs).
|
||||
WithContext(ctx).
|
||||
Post(getHnuuLoginPostUrl(loginResponseUrl))
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
WithContext(ctx).
|
||||
With("loginResponseUrl", loginResponseUrl).
|
||||
With("allInputs", allInputs).
|
||||
Wrap(err)
|
||||
})
|
||||
defer loginPostResponse.Body.Close()
|
||||
|
||||
type LoginResponse struct {
|
||||
Meta struct {
|
||||
Success bool `json:"success"`
|
||||
StatusCode int `json:"statusCode"`
|
||||
Message string `json:"message"`
|
||||
} `json:"meta"`
|
||||
Data struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"data"`
|
||||
Tgt string `json:"tgt"`
|
||||
Ticket string `json:"ticket"`
|
||||
}
|
||||
json := utils.ReadJson[LoginResponse](loginPostResponse.Body)
|
||||
if json.Tgt != "" {
|
||||
handleHnuuLoginSuccess(ctx, c.client, loginResponseUrl.Query().Get("service"), json.Ticket, json.Tgt, schoolDetail.WecloudUrl)
|
||||
} else {
|
||||
errMsg := ""
|
||||
if json.Data.Code == "CODEFALSE" {
|
||||
errMsg = "验证码错误"
|
||||
} else if json.Data.Code == "FALSE" {
|
||||
errMsg = "请检查账号密码是否正确"
|
||||
} else if json.Data.Code != "PASSERROR" {
|
||||
errMsg = "密码错误"
|
||||
} else if json.Data.Code != "NOUSER" {
|
||||
errMsg = "账号不存在"
|
||||
} else if json.Data.Code != "USERLOCK" {
|
||||
errMsg = "账号锁定"
|
||||
} else {
|
||||
errMsg = "未知错误" + json.Data.Code
|
||||
}
|
||||
utils.OkOrPanic(errors.NewBusinessError(500, errMsg))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handleHnuuLoginSuccess(ctx context.Context, client *resty.Client, url, ticket string, tgt string, host string) {
|
||||
redirectUrl := fmt.Sprintf("%s&ticket=%s", url, ticket)
|
||||
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
|
||||
response, err2 := client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get("https://hnuu.campusphere.net/iap/login")
|
||||
utils.OkOrPanic(err2)
|
||||
newUrl := response.RawResponse.Request.URL
|
||||
ticketUrl := fmt.Sprintf("https://cas.hnuu.edu.cn/lyuapServer/v1/tickets/%s", tgt)
|
||||
get, err := client.R().SetFormData(map[string]string{
|
||||
"service": url,
|
||||
"loginToken": "loginToken",
|
||||
}).Post(ticketUrl)
|
||||
utils.OkOrPanic(err)
|
||||
defer get.Body.Close()
|
||||
ticket = utils.ReadString(get.Body)
|
||||
redirectUrl = fmt.Sprintf("%s&ticket=%s", newUrl.Query().Get("service"), ticket)
|
||||
client.R().SetHeader("Referer", "https://cas.hnuu.edu.cn/").Get(redirectUrl)
|
||||
ActivateSession(ctx, host, client)
|
||||
}
|
||||
|
||||
func getHnuuLoginPostUrl(url *url.URL) string {
|
||||
// https://cas.hnuu.edu.cn/lyuapServer/v1/tickets
|
||||
return fmt.Sprintf("%s://%s/lyuapServer/v1/tickets", url.Scheme, url.Host)
|
||||
}
|
||||
|
||||
func getHnuuCaptchaUrl(url *url.URL) string {
|
||||
// https://cas.hnuu.edu.cn/lyuapServer/kaptcha?_t=1766803822&uid=
|
||||
return fmt.Sprintf("%s://%s/lyuapServer/kaptcha?_t=%d&uid=", url.Scheme, url.Host, utils.GetCurrentTimeMillis())
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) GetCookieJar() http.CookieJar {
|
||||
return c.client.CookieJar()
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) GetCookieString() string {
|
||||
cookieJar := c.GetCookieJar()
|
||||
serializableJar := cookieJar.(*cookiejar.PersistCookieJar)
|
||||
return serializableJar.Serialize()
|
||||
}
|
||||
|
||||
func (c CpdailyHnuuLogin) GetClient() *resty.Client {
|
||||
return c.client
|
||||
}
|
||||
|
||||
func NewHnuu(ctx context.Context) CpdailyLogin {
|
||||
return &CpdailyHnuuLogin{
|
||||
client: httpclient.NewCpdailyClient(),
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,32 @@ func ResolveOcrCaptcha(ctx context.Context, url string, client *resty.Client) st
|
||||
return captcha.ApiAutoOCRVerification(byteData)
|
||||
}
|
||||
|
||||
func ResolveCalcCaptcha(ctx context.Context, url string, client *resty.Client) (string, string) {
|
||||
b64, uid := DownBase64(ctx, url, client)
|
||||
return captcha.ApiAutoCalcVerification(b64), uid
|
||||
}
|
||||
|
||||
func DownBase64(ctx context.Context, url string, client *resty.Client) (string, string) {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
Get(url)
|
||||
utils.OkOrPanic(err, func(err error) error {
|
||||
return oops.
|
||||
With("captchaUrl", url).
|
||||
WithContext(ctx).
|
||||
Wrap(err)
|
||||
})
|
||||
defer res.Body.Close()
|
||||
type CaptchResponse struct {
|
||||
KaptchaType string `json:"kaptchaType"`
|
||||
UID string `json:"uid"`
|
||||
Content string `json:"content"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
json := utils.ReadJson[CaptchResponse](res.Body)
|
||||
return json.Content, json.UID
|
||||
}
|
||||
|
||||
func DownCode2Byte(ctx context.Context, url string, client *resty.Client) []byte {
|
||||
res, err := client.R().
|
||||
WithContext(ctx).
|
||||
|
||||
@@ -288,7 +288,9 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
logger.Logger.Debug("解析卡片失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
if iframes == nil {
|
||||
continue
|
||||
}
|
||||
for _, iframe := range iframes {
|
||||
if !iframe.HasData {
|
||||
continue
|
||||
@@ -319,7 +321,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
|
||||
point.ObjectID = objectID
|
||||
point.JobID = jobID
|
||||
|
||||
cords2, err := c.FetchChapterCards2(ctx, course, card.KnowledgeID)
|
||||
cords2, err := c.FetchChapterCards2(ctx, course, cardIndex, card.KnowledgeID)
|
||||
if err == nil && cords2 != nil {
|
||||
c.fillVideoPointFromCards2(cords2, &point, objectID)
|
||||
}
|
||||
@@ -524,9 +526,9 @@ func (c *XuexitongCourse) parseIframeData(htmlString string) ([]entity.IframeAtt
|
||||
}
|
||||
|
||||
// FetchChapterCards2 获取章节卡片(新接口,用于获取 otherInfo 等)
|
||||
func (c *XuexitongCourse) FetchChapterCards2(ctx context.Context, course entity.XueXiTCourse, knowledgeID int) (map[string]interface{}, error) {
|
||||
urlStr := fmt.Sprintf("%s?clazzid=%s&courseid=%s&knowledgeid=%d&num=0&ut=s&cpi=%d&v=2025-0424-1038-3&mooc2=1&isMicroCourse=false&editorPreview=0",
|
||||
ApiChapterCards2, course.Key, course.CourseID, knowledgeID, course.Cpi)
|
||||
func (c *XuexitongCourse) FetchChapterCards2(ctx context.Context, course entity.XueXiTCourse, num int, knowledgeID int) (map[string]interface{}, error) {
|
||||
urlStr := fmt.Sprintf("%s?clazzid=%s&courseid=%s&knowledgeid=%d&num=%d&ut=s&cpi=%d&v=2025-0424-1038-3&mooc2=1&isMicroCourse=false&editorPreview=0",
|
||||
ApiChapterCards2, course.Key, course.CourseID, knowledgeID, num, course.Cpi)
|
||||
|
||||
logger.Logger.Debug("FetchChapterCards2 请求",
|
||||
zap.String("url", urlStr),
|
||||
|
||||
@@ -37,7 +37,7 @@ func getAIConfig(ctx context.Context) *quesbank.AIConfig {
|
||||
func TestSchedule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
xuexitongLogin := NewXuexitongLogin(ctx)
|
||||
err := xuexitongLogin.Login(context.Background(), "13151832241", "yyz@083422", 5)
|
||||
err := xuexitongLogin.Login(context.Background(), "15754238084", "1575423yuan.dby", 5)
|
||||
utils.OkOrPanic(err)
|
||||
|
||||
client := xuexitongLogin.GetClient()
|
||||
@@ -54,7 +54,7 @@ func TestSchedule(t *testing.T) {
|
||||
logger.Logger.Warn("AI配置为空,作业将被跳过")
|
||||
}
|
||||
|
||||
err = courseService.AutoStudyByCourseIDWithOptions(ctx, "258109701", &AutoStudyOptions{
|
||||
err = courseService.AutoStudyByCourseIDWithOptions(ctx, "255690793", &AutoStudyOptions{
|
||||
AIConfig: config,
|
||||
AutoSubmit: true, // 默认不自动提交,仅保存
|
||||
Thread: 1,
|
||||
|
||||
@@ -252,6 +252,7 @@ func (c *XuexitongCourse) attachmentDetectVideo(attachment *entity.AttachmentSet
|
||||
|
||||
// AutoPlayVideoByPoint 自动播放视频(使用统一的 ChapterPoint)
|
||||
func (c *XuexitongCourse) AutoPlayVideoByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
|
||||
|
||||
if err := c.VideoDtoFetchByPoint(ctx, p); err != nil {
|
||||
return fmt.Errorf("获取视频信息失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ func init() {
|
||||
config := zap.NewProductionConfig()
|
||||
if IsDebug() {
|
||||
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) // Enable debug logs
|
||||
} else {
|
||||
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
|
||||
}
|
||||
config.EncoderConfig.EncodeTime = BeijingTimeEncoder
|
||||
|
||||
|
||||
@@ -59,19 +59,6 @@ func (s CourseServiceV1) List(ctx context.Context, req *v1.CourseListReq) *utils
|
||||
func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *utils.Response[any] {
|
||||
user := utils.GetUserInfoFromCtx(ctx)
|
||||
|
||||
// 检查是否已经存在相同的刷课任务
|
||||
exists := db2.Global.CourseEntity.Query().
|
||||
Where(
|
||||
courseentity.HasSysUserWith(sysuser.ID(user.ID)),
|
||||
courseentity.Username(req.Username),
|
||||
courseentity.CourseID(req.CourseID),
|
||||
).
|
||||
ExistX(ctx)
|
||||
|
||||
if exists {
|
||||
utils.OkOrPanic(errors2.NewBusinessError(400, "该课程已存在刷课任务"))
|
||||
}
|
||||
|
||||
// 登录学习通获取节点信息
|
||||
loginClient := xuexitong.NewXuexitongLogin(ctx)
|
||||
err := loginClient.Login(ctx, req.Username, req.Password, 3)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
func GetCurrentTimeMillis() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
Reference in New Issue
Block a user