Compare commits

...

3 Commits

Author SHA1 Message Date
sugarBecomer d14ddc212d 🐛 暂时还是单线程 没有解决好验证码问题 2025-12-19 23:20:13 +08:00
sugarBecomer 55fc94c37f 🐛 文档修复 2025-12-19 23:08:16 +08:00
sugarBecomer a590f58c6c 🐛 学习通刷课 2025-12-19 22:19:03 +08:00
12 changed files with 202 additions and 110 deletions
+3 -1
View File
@@ -121,7 +121,9 @@ func (c *XuexitongCourse) AutoCompleteWorkWithAI(ctx context.Context, p *entity.
if err != nil {
return "", fmt.Errorf("获取作业题目失败: %w", err)
}
if workDto.IsPassed {
return "", errors.New("作业已完成")
}
if len(workDto.Questions) == 0 {
return "", errors.New("作业无题目")
}
@@ -271,7 +271,8 @@ type WorkQuestion struct {
// WorkDto 作业详情
type WorkDto struct {
Title string `json:"title"` // 作业标题
Title string `json:"title"` // 作业标题
IsPassed bool `json:"isPassed"`
Cpi string `json:"cpi"` // cpi
JobId string `json:"jobId"` // 任务ID
WorkId string `json:"workId"` // 作业ID
@@ -194,7 +194,7 @@ func (c *XuexitongCourse) AutoStudyCourseWithOptions(ctx context.Context, course
return err
}
if chapter.JobCount > 0 || chapter.PointTotal > chapter.PointFinish {
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
err := c.AutoStudyChapterWithOptions(ctx, course, chapter, opts)
if err != nil {
if err == ErrTimeRestriction {
@@ -212,7 +212,7 @@ func (c *XuexitongCourse) AutoStudyCourseWithOptions(ctx context.Context, course
// 筛选需要处理的章节
var targetChapters []entity.XueXiTCourseChapter
for _, chapter := range chapters {
if chapter.JobCount > 0 || chapter.PointTotal > chapter.PointFinish {
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
targetChapters = append(targetChapters, chapter)
}
}
@@ -35,11 +35,12 @@ func ApiPassVerifyCode(client *resty.Client, code string, retryCount int) bool {
if retryCount < 0 {
return false
}
urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
//urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
urlStr := "https://mooc1-api.chaoxing.com/json/processVerify.ac"
client.SetRedirectPolicy(resty.NoRedirectPolicy())
req := client.R()
req.SetMultipartField("ucode", "", "", strings.NewReader(code))
req.SetMultipartField("app", "", "", strings.NewReader("0"))
req.SetMultipartField("app", "", "", strings.NewReader("1"))
req.Header.Add("User-Agent", GetUA("mobile"))
req.Header.Add("Accept", "*/*")
req.Header.Add("Host", "mooc1-api.chaoxing.com")
@@ -224,7 +224,10 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
if err != nil {
return err
}
// 触发202 需要重新登录获取cookie
if resp.StatusCode() == 202 {
}
body = resp.String()
if strings.Contains(body, "请输入验证码") {
logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
@@ -332,7 +335,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
point.JobID = jobID
point.DocumentType = "document"
case "insertbook":
case "insertbook", "insertreadV2":
jobID, _ := iframe.Data["_jobid"].(string)
if jobID == "" {
continue
@@ -31,6 +31,7 @@ const (
ApiVideoPlayLog = "https://mooc1.chaoxing.com/mooc-ans/multimedia/log/a"
ApiDocumentReport = "https://mooc1.chaoxing.com/ananas/job/document"
ApiDocumentBook = "https://mooc1.chaoxing.com/ananas/job"
ApiDocumentReadV2 = "https://mooc1.chaoxing.com/ananas/job/readv2"
ApiWorkFetch = "https://mooc1-api.chaoxing.com/android/mworkspecial"
ApiWorkSubmit = "https://mooc1.chaoxing.com/mooc-ans/work/addStudentWorkNew"
VideoEncKey = "d_yHJ!$pdA~5"
@@ -16,44 +16,9 @@ import (
// attachmentDetectDocument 从附件设置中检测并填充文档信息
func (c *XuexitongCourse) attachmentDetectDocument(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
for _, att := range attachment.Attachments {
property, ok := att["property"].(map[string]interface{})
if !ok {
continue
}
jobid, _ := property["jobid"].(string)
if jobid != p.JobID && p.JobID != "" {
continue
}
if name, ok := property["name"].(string); ok {
p.Title = name
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
}
break
}
}
// AttachmentsDetectionDocument 从附件设置中检测并填充文档信息
func (c *XuexitongCourse) AttachmentsDetectionDocument(attachment *entity.AttachmentSetting, p *entity.PointDocumentDto) error {
if attachment == nil {
return errors2.NewBusinessError(503, "附件设置为空")
}
for _, att := range attachment.Attachments {
typeStr := ""
// 进行不同方案的类型值获取
if t, ok := att["type"].(string); ok {
typeStr = t
} else if property, ok := att["property"].(map[string]interface{}); ok {
@@ -67,37 +32,75 @@ func (c *XuexitongCourse) AttachmentsDetectionDocument(attachment *entity.Attach
continue
}
jobid, _ := property["jobid"].(string)
if jobid != p.JobID && p.JobID != "" {
continue
}
// 如果未给出文档类型,先进行文档解析尝试
if typeStr == "" {
if name, ok := property["name"].(string); ok {
p.Title = name
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
} else {
p.IsJob = false
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
// 过滤匹配
objectid, _ := property["objectid"].(string)
jobid, _ := property["jobid"].(string)
if (p.ObjectID != "" && objectid == p.ObjectID) || (jobid != "" && p.JobID == jobid) {
break
}
} else if typeStr == "insertbook" || typeStr == "read" {
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
} else {
p.IsJob = false
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
if typeStr == "insertbook" {
p.DocumentType = "insertbook"
} else {
p.DocumentType = "read"
}
if name, ok := property["name"].(string); ok {
p.Title = name
// 过滤匹配
objectid, _ := property["objectid"].(string)
jobid, _ := property["jobid"].(string)
if (p.ObjectID != "" && objectid == p.ObjectID) || (jobid != "" && p.JobID == jobid) {
break
}
} else if typeStr == "document" || typeStr == "insertdoc" {
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
}
objectid, _ := property["objectid"].(string)
if objectid == p.ObjectID {
if name, ok := property["name"].(string); ok {
p.Title = name
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
p.DocumentType = "document"
break
}
}
if bookname, ok := property["bookname"].(string); ok {
p.Title = bookname
}
if jid, ok := property["jobid"].(string); ok {
p.JobID = jid
}
if jtoken, ok := att["jtoken"].(string); ok {
p.Jtoken = jtoken
}
if isJob, ok := att["job"].(bool); ok {
p.IsJob = isJob
}
if typeStr == "document" || typeStr == "insertdoc" {
p.Type = "document"
} else if typeStr == "insertbook" {
p.Type = "insertbook"
}
break
}
return nil
}
// DocumentDtoReadingReport 提交文档阅读完成
@@ -111,7 +114,7 @@ func (c *XuexitongCourse) DocumentDtoReadingReport(ctx context.Context, p *entit
params.Add("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
apiUrl := ApiDocumentReport
if p.Type == "insertbook" {
if p.Cpi == "insertbook" {
apiUrl = ApiDocumentBook
}
@@ -137,39 +140,26 @@ func (c *XuexitongCourse) DocumentDtoReadingReport(ctx context.Context, p *entit
return body, nil
}
// AutoReadDocument 自动阅读文档(刷课)
func (c *XuexitongCourse) AutoReadDocument(ctx context.Context, course entity.XueXiTCourse, p *entity.PointDocumentDto) error {
if !p.IsJob {
logger.Logger.Info("文档不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
return nil
}
logger.Logger.Info("开始刷文档",
zap.String("jobID", p.JobID),
zap.String("title", p.Title))
report, err := c.DocumentDtoReadingReport(ctx, p)
// AutoReadDocumentByPoint 自动阅读文档(使用统一的 ChapterPoint)
func (c *XuexitongCourse) AutoReadDocumentByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
// 获取附件信息填充 Jtoken、IsJob 等参数
attachment, _, err := c.PageMobileChapterCard(ctx, course, p.KnowledgeID, p.CardIndex)
if err != nil {
logger.Logger.Error("获取章节卡片失败", zap.String("title", p.Title), zap.Error(err))
return err
}
var result map[string]interface{}
if err := json.Unmarshal([]byte(report), &result); err == nil {
if status, ok := result["status"].(bool); ok && status {
logger.Logger.Info("文档刷课完成", zap.String("jobID", p.JobID), zap.String("title", p.Title))
c.onTaskComplete()
return nil
}
if msg, ok := result["msg"].(string); ok {
logger.Logger.Warn("文档无法正常学习", zap.String("msg", msg))
}
if attachment != nil {
c.attachmentDetectDocument(attachment, p)
}
return nil
}
logger.Logger.Debug("文档参数",
zap.String("jobID", p.JobID),
zap.String("objectID", p.ObjectID),
zap.String("jtoken", p.Jtoken),
zap.Bool("isJob", p.IsJob),
zap.String("documentType", p.DocumentType))
// AutoReadDocumentByPoint 自动阅读文档(使用统一的 ChapterPoint)
func (c *XuexitongCourse) AutoReadDocumentByPoint(ctx context.Context, course entity.XueXiTCourse, p *entity.ChapterPoint) error {
if !p.IsJob {
logger.Logger.Info("文档不是任务点,跳过", zap.String("jobID", p.JobID), zap.String("title", p.Title))
return nil
@@ -201,24 +191,28 @@ func (c *XuexitongCourse) AutoReadDocumentByPoint(ctx context.Context, course en
// DocumentDtoReadingReportByPoint 提交文档阅读完成(使用统一的 ChapterPoint)
func (c *XuexitongCourse) DocumentDtoReadingReportByPoint(ctx context.Context, p *entity.ChapterPoint) (string, error) {
params := url.Values{}
params.Add("jobid", p.JobID)
params.Add("knowledgeid", strconv.Itoa(p.KnowledgeID))
params.Add("courseid", p.CourseID)
params.Add("clazzid", p.ClassID)
params.Add("jtoken", p.Jtoken)
params.Add("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
formParams := map[string]string{
"jobid": p.JobID,
"knowledgeid": strconv.Itoa(p.KnowledgeID),
"courseid": p.CourseID,
"clazzid": p.ClassID,
"jtoken": p.Jtoken,
"_dc": strconv.FormatInt(time.Now().UnixMilli(), 10),
}
apiUrl := ApiDocumentReport
if p.DocumentType == "insertbook" {
apiUrl = ApiDocumentBook
} else if p.DocumentType == "read" {
apiUrl = ApiDocumentReadV2
}
// read 类型使用 document 接口
var body string
err := retry.Do(func() error {
resp, err := c.client.R().SetHeaders(map[string]string{
"User-Agent": GetUA("mobile"),
}).Get(apiUrl + "?" + params.Encode())
}).SetQueryParams(formParams).Get(apiUrl)
if err != nil {
return err
}
@@ -8,6 +8,7 @@ import (
"likedream-goframe/internal/framework/errors"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"strings"
"testing"
"github.com/bytedance/sonic"
@@ -56,7 +57,7 @@ func TestSchedule(t *testing.T) {
err = courseService.AutoStudyByCourseIDWithOptions(ctx, "258109701", &AutoStudyOptions{
AIConfig: config,
AutoSubmit: true, // 默认不自动提交,仅保存
Thread: 5,
Thread: 1,
})
if err != nil {
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
@@ -181,3 +182,9 @@ func TestAutoStudyAllCourses(t *testing.T) {
t.Log("所有课程刷课完成")
}
func TestSplitWords(t *testing.T) {
str := "ABCD"
split := strings.Split(str, "")
t.Log(split)
}
@@ -496,9 +496,9 @@ func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity
case entity.QuestionTypeTrueFalse:
answer := ""
for _, a := range q.Answers {
if a == "正确" {
if a == "正确" || a == "对" {
answer = "true"
} else if a == "错误" {
} else if a == "错误" || a == "错" {
answer = "false"
} else {
answer = a
+80
View File
@@ -0,0 +1,80 @@
package logger
import (
"os"
"path/filepath"
"strings"
"github.com/shirou/gopsutil/v3/process"
)
func IsDebug() bool {
return isGolandDebug() || isDebugByPath()
}
func isDebugByPath() bool {
exePath := strings.ToLower(os.Args[0])
// 获取绝对路径
absPath, err := filepath.Abs(exePath)
if err != nil {
absPath = exePath
}
// 检查路径中是否包含常见的临时目录或"go-build"字样
tmpDir := strings.ToLower(getTempDir())
isTemp := strings.Contains(absPath, tmpDir) || strings.Contains(absPath, "go-build") || strings.Contains(absPath, "go_build_")
return isTemp
}
func getTempDir() string {
tempDir := os.TempDir()
// 尝试转换为长路径
if longPath, err := filepath.EvalSymlinks(tempDir); err == nil {
if absPath, err := filepath.Abs(longPath); err == nil {
return absPath
}
return longPath
}
return filepath.Dir(tempDir)
}
func isGolandDebug() bool {
// 获取当前进程ID
pid := os.Getpid()
// 获取当前进程信息
currentProcess, err := process.NewProcess(int32(pid))
if err != nil {
return false
}
// 获取父进程
parent, err := currentProcess.Parent()
if err != nil {
return false
}
// 获取父进程名称
name, err := parent.Name()
if err != nil {
return false
}
// 检查父进程是否是dlv(Go调试器)或GoLand相关进程
name = strings.ToLower(name)
debugProcessNames := []string{
"dlv.exe", "dlv",
"goland.exe", "goland64.exe",
"goland", "idea.exe",
}
for _, debugName := range debugProcessNames {
if strings.Contains(name, debugName) {
return true
}
}
return false
}
+3
View File
@@ -13,6 +13,9 @@ var Logger *zap.Logger
func init() {
// Create a production config and customize the time encoder
config := zap.NewProductionConfig()
if IsDebug() {
config.Level = zap.NewAtomicLevelAt(zap.DebugLevel) // Enable debug logs
}
config.EncoderConfig.EncodeTime = BeijingTimeEncoder
// Build the logger with the custom config
@@ -193,7 +193,7 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
err = courseService.AutoStudyByCourseIDWithOptions(ctx, targetCourse.CourseID, &xuexitong.AutoStudyOptions{
AIConfig: aiConfig,
AutoSubmit: true, // 默认不自动提交,仅保存
Thread: 5,
Thread: 1,
})
if err != nil {
// 如果是时间限制导致的停止,不算失败