244 lines
8.2 KiB
Go
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
|
|
}
|