Compare commits

...

3 Commits

Author SHA1 Message Date
sugarBecomer 8974bd26a5 🐛 修复淮南联合大学登录功能 2025-12-27 14:36:33 +08:00
sugarBecomer 6f25652183 🐛 修复淮南联合大学登录功能 2025-12-27 14:36:16 +08:00
sugarBecomer a8f4a2afa6 🐛 修复学习通刷课问题 2025-12-22 17:25:51 +08:00
10 changed files with 245 additions and 20 deletions
@@ -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)
+180
View File
@@ -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(),
}
}
+26
View File
@@ -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)
}
+2
View File
@@ -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
-13
View File
@@ -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)
+7
View File
@@ -0,0 +1,7 @@
package utils
import "time"
func GetCurrentTimeMillis() int64 {
return time.Now().UnixMilli()
}