Files
likedream/internal/framework/schedule/xuexitong_handler.go
T
sugarBecomer 0959ca117c 🐛 学习通刷课
2025-12-19 15:47:51 +08:00

244 lines
8.2 KiB
Go

package schedule
import (
"bytes"
"context"
"encoding/gob"
"encoding/json"
"likedream-goframe/internal/application/quesbank"
"likedream-goframe/internal/application/xuexitong"
"likedream-goframe/internal/application/xuexitong/entity"
"likedream-goframe/internal/common/db"
errors2 "likedream-goframe/internal/common/errors"
"likedream-goframe/internal/ent"
"likedream-goframe/internal/ent/courseentity"
"likedream-goframe/internal/ent/sysconfig"
"likedream-goframe/internal/framework/errors"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"time"
"github.com/hibiken/asynq"
"github.com/samber/oops"
"go.uber.org/zap"
)
// getAIConfig 获取AI配置
func getAIConfig(ctx context.Context) *quesbank.AIConfig {
config, err := db.Global.SysConfig.Query().
Where(sysconfig.KeyEQ("ai.config")).
Only(ctx)
if err != nil {
logger.Logger.Debug("未配置AI,作业将跳过", zap.Error(err))
return nil
}
var aiConfig quesbank.AIConfig
if err := json.Unmarshal([]byte(config.Value), &aiConfig); err != nil {
logger.Logger.Warn("AI配置解析失败", zap.Error(err))
return nil
}
return &aiConfig
}
type XuexitongTaskHandler struct{}
func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task) (err error) {
defer utils.Pass(&err)
// 解码payload
var taskPayload XuexitongTaskPayload
e := gob.NewDecoder(bytes.NewReader(task.Payload())).Decode(&taskPayload)
utils.OkOrPanic(e, oops.WithContext(ctx).With("payload", task.Payload(), "type", task.Type()).In("XuexitongTask.DecodePayload").Wrap)
// 查询课程信息
targetCourse, e := db.Global.CourseEntity.Query().Where(courseentity.ID(taskPayload.CourseID)).First(ctx)
if ent.IsNotFound(e) {
utils.OkOrPanic(oops.WithContext(ctx).With("course_id", taskPayload.CourseID).In("XuexitongTask.ProcessTask").Wrap(e))
}
utils.OkOrPanic(e, oops.WithContext(ctx).With("course_id", taskPayload.CourseID).In("XuexitongTask.ProcessTask").Wrap)
// 检查是否暂停
if targetCourse.Status == courseentity.StatusPause {
logger.Logger.Debug("课程已暂停,跳过执行", zap.Int64("courseID", targetCourse.ID))
return nil
}
// 检查时间限制(非暴力模式)
if targetCourse.Violence == courseentity.ViolenceNo {
currentTime := time.Now()
currentHour := currentTime.Hour()
currentMinute := currentTime.Minute()
// 只在 8:30-17:30 之间执行
startTime := 8*60 + 30 // 8:30
endTime := 17*60 + 30 // 17:30
currentMinutes := currentHour*60 + currentMinute
if currentMinutes < startTime || currentMinutes >= endTime {
return nil
}
}
// 更新状态为执行中
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusDoing).ExecX(ctx)
defer utils.HandleError(func(err error) {
// 执行失败更新状态
e := db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusFail).Exec(ctx)
utils.LogError(ctx, "设置课程状态失败", oops.With("course_id", taskPayload.CourseID).Wrap(e))
var errorMessage string
if e1, ok := utils.Of[*errors.BusinessError](err); ok {
errorMessage = e1.Message()
} else if e1, ok := utils.Of[*errors2.BusinessError](err); ok {
errorMessage = e1.Message
} else {
errorMessage = "未知的错误导致任务执行失败: " + err.Error()
}
logger.Logger.Error("学习通刷课失败",
zap.String("courseName", targetCourse.CourseName),
zap.String("error", errorMessage))
panic(err)
})
// 创建学习通登录客户端
loginClient := xuexitong.NewXuexitongLogin(ctx)
// 登录学习通
err = loginClient.Login(ctx, targetCourse.Username, targetCourse.Password, 3)
if err != nil {
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(400, "登录失败,请检查账号密码", err))
}
// 获取客户端
client := loginClient.GetClient()
// 创建课程服务
courseService := xuexitong.NewXuexitongCourse(client)
// 非暴力模式下,设置时间检查回调,每个任务点完成后都会检查时间
if targetCourse.Violence == courseentity.ViolenceNo {
courseService.SetTimeCheckFunc(func() bool {
currentTime := time.Now()
currentHour := currentTime.Hour()
currentMinute := currentTime.Minute()
startTime := 8*60 + 30 // 8:30
endTime := 17*60 + 30 // 17:30
currentMinutes := currentHour*60 + currentMinute
needStudy := currentMinutes >= startTime && currentMinutes < endTime
if !needStudy {
// 更新状态为等待中
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusWaiting).ExecX(ctx)
}
return needStudy
})
}
// 先获取课程列表找到目标课程
courseList := courseService.PullCourseList(ctx)
var course *entity.XueXiTCourse
for _, c := range courseList {
if c.CourseID == targetCourse.CourseID {
course = &c
break
}
}
if course == nil {
utils.OkOrPanic(errors.NewBusinessError(404, "未找到课程"))
}
// 开始刷课前,先获取并更新任务点统计
chapters := courseService.PullCourseChapterList(ctx, *course)
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
_, statusErr := courseService.FetchChapterPointStatus(ctx, *course, chapters)
if statusErr != nil {
logger.Logger.Warn("获取章节任务点状态失败", zap.Error(statusErr))
} else {
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
var totalPointCount, finishedPointCount int
for _, chapter := range chapters {
totalPointCount += chapter.PointTotal
finishedPointCount += chapter.PointFinish
}
logger.Logger.Info("课程任务点统计",
zap.String("courseName", targetCourse.CourseName),
zap.Int("totalPointCount", totalPointCount),
zap.Int("finishedPointCount", finishedPointCount))
// 更新数据库
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
SetTaskCount(totalPointCount).
SetSuccessCount(finishedPointCount).
ExecX(ctx)
}
// 设置任务点完成回调,每完成一个任务点更新 success_count
courseService.SetTaskCompleteCallback(func() {
// 增加 success_count
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
AddSuccessCount(1).
ExecX(ctx)
})
// 获取AI配置(用于作业答题)
aiConfig := getAIConfig(ctx)
// 执行刷课(带AI配置)
err = courseService.AutoStudyByCourseIDWithOptions(ctx, targetCourse.CourseID, &xuexitong.AutoStudyOptions{
AIConfig: aiConfig,
AutoSubmit: true, // 默认不自动提交,仅保存
Thread: 5,
})
if err != nil {
// 如果是时间限制导致的停止,不算失败
if err == xuexitong.ErrTimeRestriction {
logger.Logger.Info("课程因时间限制暂停刷课,等待下次执行", zap.Int64("courseID", targetCourse.ID))
// 更新状态为等待中,等待下次定时任务
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).SetStatus(courseentity.StatusWaiting).ExecX(ctx)
return nil
}
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
}
// 刷课完成后,重新统计任务情况
logger.Logger.Info("刷课完成,正在重新统计任务情况...")
// 重新获取章节列表和任务点状态
chaptersAfter := courseService.PullCourseChapterList(ctx, *course)
// FetchChapterPointStatus 会更新 chapters 中每个章节的 PointTotal 和 PointFinish
_, statusErrAfter := courseService.FetchChapterPointStatus(ctx, *course, chaptersAfter)
var totalPointCountAfter, finishedPointCountAfter int
if statusErrAfter != nil {
logger.Logger.Warn("获取章节任务点状态失败", zap.Error(statusErrAfter))
} else {
// 通过累加每个章节的 PointTotal 和 PointFinish 计算总数
for _, chapter := range chaptersAfter {
totalPointCountAfter += chapter.PointTotal
finishedPointCountAfter += chapter.PointFinish
}
}
logger.Logger.Info("课程统计完成",
zap.String("courseName", targetCourse.CourseName),
zap.Int("totalPointCount", totalPointCountAfter),
zap.Int("finishedPointCount", finishedPointCountAfter))
// 更新数据库
db.Global.CourseEntity.UpdateOneID(taskPayload.CourseID).
SetStatus(courseentity.StatusDone).
SetTaskCount(totalPointCountAfter).
SetSuccessCount(finishedPointCountAfter).
ExecX(ctx)
logger.Logger.Info("学习通刷课完成", zap.String("courseName", targetCourse.CourseName))
return nil
}