Compare commits

...

7 Commits

Author SHA1 Message Date
sugarBecomer a8f4a2afa6 🐛 修复学习通刷课问题 2025-12-22 17:25:51 +08:00
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
sugarBecomer 0959ca117c 🐛 学习通刷课 2025-12-19 15:47:51 +08:00
sugarBecomer 3641fb1782 🐛 学习通答题 2025-12-12 11:49:57 +08:00
sugarBecomer d0e7652bf6 🐛 学习通答题 2025-12-12 11:37:33 +08:00
15 changed files with 341 additions and 215 deletions
+2 -2
View File
@@ -50,8 +50,8 @@ func Run() {
g.Log().Info(ctx, "server is going to shut down...")
api.StopServer()
g.Log().Info(ctx, "api server stopped")
//schedule.Stop()
//g.Log().Info(ctx, "schedule stopped")
schedule.Stop()
g.Log().Info(ctx, "schedule stopped")
redis.Shutdown()
g.Log().Info(ctx, "redis client closed")
os.Exit(0)
+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
@@ -89,39 +89,7 @@ func (c *XuexitongCourse) AutoStudyAllCourses(ctx context.Context) error {
// AutoStudyCourse 自动学习指定课程的所有章节
func (c *XuexitongCourse) AutoStudyCourse(ctx context.Context, course entity.XueXiTCourse) error {
logger.Logger.Info(fmt.Sprintf("开始刷课程: %s", course.CourseName))
chapters := c.PullCourseChapterList(ctx, course)
if len(chapters) == 0 {
logger.Logger.Warn(fmt.Sprintf("课程 %s 没有章节", course.CourseName))
return nil
}
_, err := c.FetchChapterPointStatus(ctx, course, chapters)
if err != nil {
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
}
for _, chapter := range chapters {
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
return err
}
if chapter.JobCount > 0 || chapter.PointTotal > chapter.PointFinish {
err := c.AutoStudyChapter(ctx, course, chapter)
if err != nil {
if err == ErrTimeRestriction {
return err
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
continue
}
}
}
logger.Logger.Info(fmt.Sprintf("课程完成: %s", course.CourseName))
return nil
return c.AutoStudyCourseWithOptions(ctx, course, nil)
}
// AutoStudyChapter 自动学习指定章节的所有任务点
@@ -154,6 +122,7 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
for i := range points {
if err := c.checkTimeRestriction(); err != nil {
// 更新任务状态为waiting
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", chapter.Name))
return err
}
@@ -193,6 +162,7 @@ func (c *XuexitongCourse) AutoStudyChapter(ctx context.Context, course entity.Xu
type AutoStudyOptions struct {
AIConfig *AIConfig // AI配置(用于作业答题)
AutoSubmit bool // 是否自动提交作业
Thread int // 线程数
}
// AutoStudyCourseWithOptions 带选项的自动学习课程
@@ -210,21 +180,90 @@ func (c *XuexitongCourse) AutoStudyCourseWithOptions(ctx context.Context, course
logger.Logger.Warn("获取章节状态失败", zap.Error(err))
}
for _, chapter := range chapters {
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
return err
// 获取线程数配置,默认为1(单线程)
threadCount := 1
if opts != nil && opts.Thread > 0 {
threadCount = opts.Thread
}
// 如果是单线程,使用原有逻辑
if threadCount == 1 {
for _, chapter := range chapters {
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("course", course.CourseName))
return err
}
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
err := c.AutoStudyChapterWithOptions(ctx, course, chapter, opts)
if err != nil {
if err == ErrTimeRestriction {
return err
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
continue
}
}
}
} else {
// 多线程并发处理
logger.Logger.Info(fmt.Sprintf("使用 %d 线程并发刷课", threadCount))
// 筛选需要处理的章节
var targetChapters []entity.XueXiTCourseChapter
for _, chapter := range chapters {
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
targetChapters = append(targetChapters, chapter)
}
}
if chapter.JobCount > 0 && chapter.PointTotal > chapter.PointFinish {
//logger.Logger.Info("开始处理章节", zap.String("chapter", chapter.Name))
err := c.AutoStudyChapterWithOptions(ctx, course, chapter, opts)
if err != nil {
if err == ErrTimeRestriction {
return err
if len(targetChapters) == 0 {
logger.Logger.Info("没有需要处理的章节")
return nil
}
// 创建信号量控制并发数
semaphore := make(chan struct{}, threadCount)
errChan := make(chan error, len(targetChapters))
doneChan := make(chan bool, len(targetChapters))
// 并发处理章节
for _, chapter := range targetChapters {
semaphore <- struct{}{} // 获取信号量
go func(ch entity.XueXiTCourseChapter) {
defer func() {
<-semaphore // 释放信号量
doneChan <- true
}()
if err := c.checkTimeRestriction(); err != nil {
logger.Logger.Info("时间限制,暂停刷课", zap.String("chapter", ch.Name))
errChan <- err
return
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", chapter.Name), zap.Error(err))
continue
err := c.AutoStudyChapterWithOptions(ctx, course, ch, opts)
if err != nil {
if err == ErrTimeRestriction {
errChan <- err
return
}
logger.Logger.Error(fmt.Sprintf("刷章节失败: %s", ch.Name), zap.Error(err))
}
}(chapter)
}
// 等待所有任务完成
for i := 0; i < len(targetChapters); i++ {
<-doneChan
}
close(errChan)
// 检查是否有时间限制错误
for err := range errChan {
if err == ErrTimeRestriction {
return err
}
}
}
@@ -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智能识别绕过.....")
@@ -285,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
@@ -302,6 +307,7 @@ func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.X
CourseID: course.CourseID,
ClassID: course.Key,
Cpi: cpiStr,
Title: card.Title,
}
switch pointType {
@@ -315,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)
}
@@ -331,7 +337,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
@@ -520,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),
@@ -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"
@@ -36,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()
@@ -53,9 +54,10 @@ 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,
})
if err != nil {
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
@@ -180,3 +182,9 @@ func TestAutoStudyAllCourses(t *testing.T) {
t.Log("所有课程刷课完成")
}
func TestSplitWords(t *testing.T) {
str := "ABCD"
split := strings.Split(str, "")
t.Log(split)
}
@@ -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)
}
@@ -439,32 +439,32 @@ func cleanText(text string) string {
// SubmitWorkAnswers 提交作业答案
func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity.WorkDto, isSave bool) (string, error) {
formData := url.Values{}
pyFlag := ""
if isSave {
pyFlag = "1"
}
formData.Set("pyFlag", pyFlag)
formData.Set("courseId", workDto.CourseId)
formData.Set("classId", workDto.ClassId)
formData.Set("api", workDto.Api)
formData.Set("workAnswerId", workDto.WorkAnswerId)
formData.Set("answerId", workDto.AnswerId)
formData.Set("totalQuestionNum", workDto.TotalQuestionNum)
formData.Set("fullScore", workDto.FullScore)
formData.Set("knowledgeid", workDto.KnowledgeId)
formData.Set("oldSchoolId", workDto.OldSchoolId)
formData.Set("oldWorkId", workDto.OldWorkId)
formData.Set("jobid", workDto.JobId)
formData.Set("workRelationId", workDto.WorkRelationId)
formData.Set("enc", "")
formData.Set("enc_work", workDto.EncWork)
formData.Set("userId", workDto.UserId)
formData.Set("cpi", workDto.Cpi)
formData.Set("workTimesEnc", "")
formData.Set("randomOptions", "false")
formData.Set("isAccessibleCustomFid", "0")
formMap := map[string]string{
"pyFlag": pyFlag,
"courseId": workDto.CourseId,
"classId": workDto.ClassId,
"api": workDto.Api,
"mooc": "0",
"workAnswerId": workDto.WorkAnswerId,
"answerId": "",
"totalQuestionNum": workDto.TotalQuestionNum,
"fullScore": workDto.FullScore,
"knowledgeid": workDto.KnowledgeId,
"oldSchoolId": workDto.OldSchoolId,
"oldWorkId": workDto.OldWorkId,
"jobid": workDto.JobId,
"workRelationId": workDto.WorkRelationId,
"enc_work": workDto.EncWork,
"isphone": "true",
"userId": workDto.UserId,
"workTimesEnc": "",
"isAccessibleCustomFid": "0",
}
answerwqbid := ""
for _, q := range workDto.Questions {
if q.Qid != "" {
@@ -474,46 +474,46 @@ func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity
switch q.Type {
case entity.QuestionTypeSingleChoice:
answer := strings.Join(q.Answers, "")
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "0")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "0"
case entity.QuestionTypeMultipleChoice:
answer := strings.Join(q.Answers, "")
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "1")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "1"
case entity.QuestionTypeFillBlank:
for key, val := range q.BlankAnswer {
re := regexp.MustCompile(`\d+`)
numbers := re.FindAllString(key, -1)
if len(numbers) > 0 {
formData.Set("answer"+q.Qid+numbers[0], val)
formMap["answer"+q.Qid+numbers[0]] = val
}
}
formData.Set("tiankongsize"+q.Qid, strconv.Itoa(q.BlankCount))
formData.Set("answertype"+q.Qid, "2")
formMap["tiankongsize"+q.Qid] = strconv.Itoa(q.BlankCount)
formMap["answertype"+q.Qid] = "2"
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
}
}
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "3")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "3"
case entity.QuestionTypeShortAnswer:
answer := strings.Join(q.Answers, "")
formData.Set("answer"+q.Qid, answer)
formData.Set("answertype"+q.Qid, "4")
formMap["answer"+q.Qid] = answer
formMap["answertype"+q.Qid] = "4"
}
}
formData.Set("answerwqbid", answerwqbid)
formMap["answerwqbid"] = answerwqbid
urlStr := fmt.Sprintf("%s?_classId=%s&courseid=%s&token=%s&totalQuestionNum=%s&ua=pc&formType=post&saveStatus=1&version=1&tempsave=1",
ApiWorkSubmit, workDto.ClassId, workDto.CourseId, workDto.EncWork, workDto.TotalQuestionNum)
@@ -523,7 +523,7 @@ func (c *XuexitongCourse) SubmitWorkAnswers(ctx context.Context, workDto *entity
resp, err := c.client.R().SetHeaders(map[string]string{
"User-Agent": GetUA("mobile"),
"Content-Type": "application/x-www-form-urlencoded",
}).SetBody(formData.Encode()).Post(urlStr)
}).SetFormData(formMap).Post(urlStr)
if err != nil {
return err
}
+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
}
+5
View File
@@ -13,6 +13,11 @@ 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
} else {
config.Level = zap.NewAtomicLevelAt(zap.InfoLevel)
}
config.EncoderConfig.EncodeTime = BeijingTimeEncoder
// Build the logger with the custom config
@@ -78,10 +78,6 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
currentMinutes := currentHour*60 + currentMinute
if currentMinutes < startTime || currentMinutes >= endTime {
// logger.Logger.Debug("非暴力模式,当前时间不在执行时间范围内(8:30-17:30),跳过执行",
// zap.Int64("courseID", targetCourse.ID),
// zap.Int("currentHour", currentHour),
// zap.Int("currentMinute", currentMinute))
return nil
}
}
@@ -134,8 +130,12 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
startTime := 8*60 + 30 // 8:30
endTime := 17*60 + 30 // 17:30
currentMinutes := currentHour*60 + currentMinute
return currentMinutes >= startTime && currentMinutes < endTime
needStudy := currentMinutes >= startTime && currentMinutes < endTime
if !needStudy {
// 更新状态为等待中
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusWaiting).ExecX(ctx)
}
return needStudy
})
}
@@ -192,7 +192,8 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
// 执行刷课(带AI配置)
err = courseService.AutoStudyByCourseIDWithOptions(ctx, targetCourse.CourseID, &xuexitong.AutoStudyOptions{
AIConfig: aiConfig,
AutoSubmit: false, // 默认不自动提交,仅保存
AutoSubmit: true, // 默认不自动提交,仅保存
Thread: 1,
})
if err != nil {
// 如果是时间限制导致的停止,不算失败
-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)