✨ 学习通 & 销假

This commit is contained in:
sugarBecomer
2025-12-05 22:09:47 +08:00
parent 921192a914
commit 3a2e679b85
16 changed files with 1969 additions and 1232 deletions
+3 -1
View File
@@ -51,6 +51,8 @@ sliderIds = ["1018764144934965", "sdhyxy", "hqdx"]
[basePoint]
cpdaily = "0.5"
cpdailyTask = "0.5"
cpdailyLeave = "4"
[task]
concurrency = 20
+1
View File
@@ -59,6 +59,7 @@ func StartServer(ctx context.Context) {
cpdailyGroup.Bind(cpdaily.NewCpTaskCtrlV1())
cpdailyGroup.Bind(cpdaily.NewCpImageCtrlV1())
cpdailyGroup.Bind(cpdaily.NewCpTaskLogCtrlV1())
cpdailyGroup.Bind(cpdaily.NewCpLeaveCtrlV1())
// 娱乐
musicGroup := group.Group("/music").Middleware(middleware.AuthHandler, middleware.PermissionHandler)
musicGroup.Bind(music.NewMusicCtrlV1())
+7 -2
View File
@@ -62,12 +62,17 @@ func (c *CpdailyReportApp) GetLeaveList() []leavetype.LeaveListOne {
var listList []leavetype.LeaveListOne
for _, row := range rows.Array() {
// 已完成
if row.Get("status").String() == "4" {
continue
}
leaveOne := leavetype.LeaveListOne{
Id: row.Get("id").String(),
LeaveType: row.Get("leaveType").String(),
CreateTime: row.Get("createTime").String(),
Reason: row.Get("reason").String(),
AceEndTime: row.Get("aceEndTime").String(),
Reason: row.Get("leaveReason").String(),
AceEndTime: row.Get("actEndTime").String(),
Status: row.Get("status").String(),
}
listList = append(listList, leaveOne)
}
@@ -6,4 +6,5 @@ type LeaveListOne struct {
CreateTime string `json:"createTime"`
AceEndTime string `json:"aceEndTime"`
Reason string `json:"reason"`
Status string `json:"status"`
}
@@ -62,44 +62,33 @@ type XueXiTCourse struct {
CourseID string `json:"courseId"` // 课程ID
ChatID string `json:"chatId"`
CourseTeacher string `json:"courseTeacher"` // 课程老师
CourseName string `json:"courseName"` //课程名
CourseName string `json:"courseName"` // 课程名
CourseImage string `json:"courseImage"`
// 两个标识 暂时不知道有什么用
CourseDataID int `json:"courseDataId"`
ContentID int `json:"ContentID"`
IsStart bool `json:"isstart"` //是否开课了,开课了为true,没开课为false。一般来说开课才能刷,不然刷不了的
State int `json:"state"` //课程状态,0为正常,1为课程已经结束
IsStart bool `json:"isstart"` // 是否开课了,开课了为true,没开课为false
State int `json:"state"` // 课程状态,0为正常,1为课程已经结束
}
// XueXiTCourseChapter 课程章节信息
type XueXiTCourseChapter struct {
ID string `json:"id"` // 章节ID
ID int `json:"id"` // 章节ID
Name string `json:"name"` // 章节名称
ParentNodeID string `json:"parentnodeid"` // 父节点ID
ParentNodeID int `json:"parentnodeid"` // 父节点ID
Layer int `json:"layer"` // 层级
Label string `json:"label"` // 标签
IndexOrder int `json:"indexOrder"` // 排序索引
JobCount int `json:"jobcount"` // 任务点数量
Status int `json:"status"` // 状态
Status string `json:"status"` // 状态
IsReview int `json:"isReview"` // 是否复习
BeginTime int64 `json:"begintime"` // 开始时间
EndTime int64 `json:"endtime"` // 结束时间
Attachment []XueXiTAttachment `json:"attachment"` // 附件列表
PointTotal int `json:"pointTotal"` // 总任务点
PointFinish int `json:"pointFinish"` // 已完成任务点
Knowledge []XueXiTCourseChapter `json:"knowledge"` // 子章节列表
}
// XueXiTAttachment 附件信息
type XueXiTAttachment struct {
ID string `json:"id"` // 附件ID
Type string `json:"type"` // 附件类型(video/document等)
ObjectID string `json:"objectid"` // 对象ID
Extension string `json:"extension"` // 扩展信息
JobID string `json:"jobid"` // 任务ID
Property string `json:"property"` // 属性
Name string `json:"name"` // 附件名称
IsPassed bool `json:"isPassed"` // 是否已完成
}
// ChapterData 章节数据响应
type ChapterData struct {
Data []struct {
@@ -133,20 +122,7 @@ type ChapterData struct {
Belongschoolid string `json:"belongschoolid"`
Objectid string `json:"objectid"`
Knowledge struct {
Data []struct {
Jobcount int `json:"jobcount"`
Isreview int `json:"isreview"`
Attachment struct {
Data []interface{} `json:"data"`
} `json:"attachment"`
Indexorder int `json:"indexorder"`
Name string `json:"name"`
ID int `json:"id"`
Label string `json:"label"`
Layer int `json:"layer"`
Parentnodeid int `json:"parentnodeid"`
Status string `json:"status"`
} `json:"data"`
Data []KnowledgeData `json:"data"`
} `json:"knowledge"`
} `json:"data"`
} `json:"course"`
@@ -157,152 +133,15 @@ type ChapterData struct {
} `json:"data"`
}
// Card 任务点卡片信息
type Card struct {
ID string `json:"id"` // 卡片ID
Title string `json:"title"` // 标题
Description string `json:"description"` // 描述
KnowledgeID string `json:"knowledgeid"` // 知识点ID
Type string `json:"type"` // 类型(video/document/work等)
IsPassed bool `json:"isPassed"` // 是否完成
ObjectID string `json:"objectid"` // 对象ID
JobID string `json:"jobid"` // 任务ID
ResourceID string `json:"resourceid"` // 资源ID
Attachment *IframeAttributes `json:"attachment,omitempty"` // iframe属性
}
// IframeAttributes iframe属性
type IframeAttributes struct {
Src string `json:"src"` // iframe地址
Module string `json:"module"` // 模块名
ModToken string `json:"modtoken"` // 模块token
Width string `json:"width"` // 宽度
Height string `json:"height"` // 高度
Scrolling string `json:"scrolling"` // 滚动
ClassID string `json:"classid"` // 课程ID
ObjectID string `json:"objectid"` // 对象ID
Enc string `json:"enc"` // 加密参数
UtEnc string `json:"utenc"` // ut加密参数
OtherInfo string `json:"otherinfo"` // 其他信息
JobID string `json:"jobid"` // 任务ID
KnowledgeID string `json:"knowledgeid"` // 知识点ID
CourseFaceCheck int `json:"coursefacecheck"` // 人脸识别
}
// DataItem 任务点数据项
type DataItem struct {
ID string `json:"id"`
IndexOrder int `json:"indexorder"`
KnowledgeID string `json:"knowledgeid"`
Attachment struct {
ObjectID string `json:"objectid"`
Property struct {
Name string `json:"name"`
IsPassed bool `json:"ispassed"`
JobID string `json:"jobid"`
} `json:"property"`
Type string `json:"type"`
} `json:"attachment"`
PointList []interface{} `json:"pointList"`
JobCount int `json:"jobcount"`
Duration int64 `json:"duration"`
ClazzID string `json:"clazzid"`
CourseID string `json:"courseid"`
Module string `json:"module"`
}
// VideoDto 视频信息
type VideoDto struct {
VideoPath string `json:"videoPath"` // 视频路径
ObjectID string `json:"objectid"` // 对象ID
OtherInfo string `json:"otherInfo"` // 其他信息
JobID string `json:"jobid"` // 任务ID
Duration int `json:"duration"` // 时长(秒)
DToken string `json:"dtoken"` // dtoken
IsAudio bool `json:"isAudio"` // 是否是音频
IsPassed bool `json:"isPassed"` // 是否已完成
}
// DocumentDto 文档信息
type DocumentDto struct {
ObjectID string `json:"objectid"` // 对象ID
JobID string `json:"jobid"` // 任务ID
OtherInfo string `json:"otherInfo"` // 其他信息
JToken string `json:"jtoken"` // jtoken
PageCount int `json:"pageCount"` // 页数
IsPassed bool `json:"isPassed"` // 是否已完成
}
// FaceQRCodeResponse 人脸识别二维码响应
type FaceQRCodeResponse struct {
Status bool `json:"status"`
Msg string `json:"msg"`
QR string `json:"qr"` // 二维码base64数据
Enc string `json:"enc"` // 加密参数
}
// FaceTokenResponse 人脸识别上传令牌响应
type FaceTokenResponse struct {
Result int `json:"result"`
UploadToken string `json:"uploadtoken"`
}
// FaceUploadResponse 人脸识别上传响应
type FaceUploadResponse struct {
Result bool `json:"result"`
Msg string `json:"msg"`
Data struct {
FileID string `json:"fileId"`
} `json:"data"`
}
// FaceVerifyResponse 人脸识别验证响应
type FaceVerifyResponse struct {
Status bool `json:"status"`
Msg string `json:"msg"`
}
// WorkDto 作业信息
type WorkDto struct {
WorkID string `json:"workid"` // 作业ID
CourseID string `json:"courseid"` // 课程ID
ClassID string `json:"classid"` // 班级ID
KnowledgeID string `json:"knowledgeid"` // 知识点ID
JobID string `json:"jobid"` // 任务ID
Enc string `json:"enc"` // 加密参数
UTEnc string `json:"utenc"` // UT加密参数
Title string `json:"title"` // 作业标题
IsPassed bool `json:"isPassed"` // 是否已完成
Questions []WorkQuestion `json:"questions"` // 题目列表
}
// WorkQuestion 作业题目
type WorkQuestion struct {
QuestionID string `json:"questionId"` // 题目ID
Type int `json:"type"` // 题目类型: 0单选 1多选 2判断 3填空 4简答
Title string `json:"title"` // 题目标题
Description string `json:"description"` // 题目描述
Options []string `json:"options"` // 选项列表(单选/多选/判断)
Answer string `json:"answer"` // 答案
Score float64 `json:"score"` // 分值
}
// WorkSubmitRequest 作业提交请求
type WorkSubmitRequest struct {
WorkID string `json:"workid"`
TotalScore string `json:"totalScore"`
AnswerList []WorkAnswerItem `json:"answerlist"`
UserId string `json:"userId"`
ClassId string `json:"classId"`
CourseId string `json:"courseId"`
KnowledgeId string `json:"knowledgeid"`
JobId string `json:"jobid"`
Enc string `json:"enc"`
UTEnc string `json:"utenc"`
}
// WorkAnswerItem 作业答案项
type WorkAnswerItem struct {
QuestionID string `json:"questionId"`
Answer string `json:"answer"`
// KnowledgeData 章节知识点数据
type KnowledgeData struct {
Jobcount int `json:"jobcount"`
Isreview int `json:"isreview"`
Indexorder int `json:"indexorder"`
Name string `json:"name"`
ID int `json:"id"`
Label string `json:"label"`
Layer int `json:"layer"`
Parentnodeid int `json:"parentnodeid"`
Status string `json:"status"`
}
@@ -0,0 +1,223 @@
package entity
// PointType 任务点类型
type PointType string
const (
PointTypeVideo PointType = "insertvideo"
PointTypeDocument PointType = "insertdoc"
PointTypeDocument2 PointType = "document"
PointTypeBook PointType = "insertbook"
PointTypeWork PointType = "insertwork"
PointTypeHyperlink PointType = "insertlink"
PointTypeLive PointType = "insertlive"
)
// PointVideoDto 视频任务点
type PointVideoDto struct {
CardIndex int
CourseID string
ClassID string
KnowledgeID int
Cpi string
ObjectID string
// 从SSR视图中获取
IsPassed bool
FID int
DToken string
PlayTime int
Duration int
JobID string
OtherInfo string
Title string
RT float64
VideoFaceCaptureEnc string
RandomCaptureTime string
AttDurationEnc string
Enc string
Mid string
}
// PointDocumentDto 文档查看任务点
type PointDocumentDto struct {
CardIndex int
CourseID string
ClassID string
KnowledgeID int
Cpi string
ObjectID string
Title string
JobID string
Jtoken string
IsJob bool // 是否是任务点
Type string // document / insertbook
}
// PointWorkDto 测验/作业任务点
type PointWorkDto struct {
CardIndex int
CourseID string
ClassID string
KnowledgeID int
Cpi string
WorkID string
SchoolID string
JobID string
PUID string
KToken string
Enc string
Title string
IsPassed bool
}
// PointHyperlinkDto 外链任务点
type PointHyperlinkDto struct {
CardIndex int
CourseID string
ClassID string
KnowledgeID int
Cpi string
ObjectID string
Title string
JobID string
Jtoken string
LinkType int // 外链类型
IsJob bool // 是否是任务点
}
// PointLiveDto 直播任务点
type PointLiveDto struct {
CardIndex int
CourseID string
ClassID string
KnowledgeID int
Cpi string
UserId string // 用户ID
Live bool
LiveId string // 直播ID
Vdoid string
Mid string
Title string // 直播标题
JobID string
StreamName string // 直播流名称
LiveStatusStr string // 直播状态文字
LiveStatusCode int // 直播状态码 0=未开始
VideoDuration int // 视频时长
Aid string
Module string // 类型
IsJob bool // 是否为任务点
AuthEnc string
LiveDragEnc string
LiveSetEnc string
OtherInfo string
Enc string
LiveSwDsEnc string
VideoCompletePercent float64 // 观看进度
}
// Card 任务点卡片信息
type Card struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
CardOrder int `json:"cardorder"`
KnowledgeID int `json:"knowledgeid"`
}
// CardAPIResponse 卡片API响应
type CardAPIResponse struct {
Data []CardDataItem `json:"data"`
}
// CardDataItem 卡片数据项
type CardDataItem struct {
ClickCount int `json:"clickcount"`
Createtime int64 `json:"createtime"`
OpenLock int `json:"openlock"`
IndexOrder int `json:"indexorder"`
Name string `json:"name"`
LastModifyTime int64 `json:"lastmodifytime"`
ID int `json:"id"`
Label string `json:"label"`
Layer int `json:"layer"`
Card struct {
Data []Card `json:"data"`
} `json:"card"`
ParentNodeID int `json:"parentnodeid"`
Status string `json:"status"`
}
// IframeAttributes iframe 的属性
type IframeAttributes struct {
Data map[string]interface{} `json:"data"`
Other map[string]string
HasData bool
}
// ChapterPointStatus 章节任务点状态
type ChapterPointStatus map[string]struct {
ClickCount int `json:"clickcount"`
FinishCount int `json:"finishcount"`
TotalCount int `json:"totalcount"`
OpenLock int `json:"openlock"`
UnFinishCount int `json:"unfinishcount"`
}
// AttachmentSetting 附件设置(从页面JS中解析)
type AttachmentSetting struct {
Attachments []map[string]interface{} `json:"attachments"`
Defaults map[string]interface{} `json:"defaults"`
}
// ChapterPoints 章节所有任务点集合
type ChapterPoints struct {
Videos []PointVideoDto
Documents []PointDocumentDto
Works []PointWorkDto
Hyperlinks []PointHyperlinkDto
Lives []PointLiveDto
}
// QuestionType 题目类型
type QuestionType int
const (
QuestionTypeSingleChoice QuestionType = 0 // 单选题
QuestionTypeMultipleChoice QuestionType = 1 // 多选题
QuestionTypeFillBlank QuestionType = 2 // 填空题
QuestionTypeTrueFalse QuestionType = 3 // 判断题
QuestionTypeShortAnswer QuestionType = 4 // 简答题
)
// WorkQuestion 作业题目
type WorkQuestion struct {
Qid string `json:"qid"` // 题目ID
Type QuestionType `json:"type"` // 题目类型
Text string `json:"text"` // 题目内容
Options map[string]string `json:"options"` // 选项 (A: xxx, B: xxx)
Answers []string `json:"answers"` // 答案
BlankCount int `json:"blankCount"` // 填空题空格数量
BlankAnswer map[string]string `json:"blankAnswer"` // 填空题答案 (第1空: xxx)
}
// WorkDto 作业详情
type WorkDto struct {
Title string `json:"title"` // 作业标题
Cpi string `json:"cpi"` // cpi
JobId string `json:"jobId"` // 任务ID
WorkId string `json:"workId"` // 作业ID
ClassId string `json:"classId"` // 班级ID
CourseId string `json:"courseId"` // 课程ID
KnowledgeId string `json:"knowledgeId"` // 知识点ID
UserId string `json:"userId"` // 用户ID
FullScore string `json:"fullScore"` // 满分
OldWorkId string `json:"oldWorkId"` // 原始作业ID
OldSchoolId string `json:"oldSchoolId"` // 原始学校ID
Api string `json:"api"` // API值
WorkRelationId string `json:"workRelationId"` // 作业关联ID
EncWork string `json:"encWork"` // 加密参数
WorkAnswerId string `json:"workAnswerId"` // 作业答案ID
AnswerId string `json:"answerId"` // 答案ID
TotalQuestionNum string `json:"totalQuestionNum"` // 题目总数
Questions []WorkQuestion `json:"questions"` // 题目列表
}
@@ -9,10 +9,8 @@ import (
"resty.dev/v3"
)
var randChar []string = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f", "A", "B", "C", "D", "E", "F"}
// ApiVerifyCodeBytes 获取学习通验证码图片
func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
urlStr := "https://mooc1-api.chaoxing.com/processVerifyPng.ac?t=" + fmt.Sprintf("%d", time.Now().UnixMilli())
req := client.R()
@@ -32,8 +30,9 @@ func ApiVerifyCodeBytes(client *resty.Client) ([]byte, error) {
return all, nil
}
func ApiPassVerifyCode(client *resty.Client, code string, retry int) bool {
if retry < 0 {
// ApiPassVerifyCode 提交学习通验证码
func ApiPassVerifyCode(client *resty.Client, code string, retryCount int) bool {
if retryCount < 0 {
return false
}
urlStr := "https://mooc1-api.chaoxing.com/html/processVerify.ac"
@@ -47,7 +46,7 @@ func ApiPassVerifyCode(client *resty.Client, code string, retry int) bool {
req.Header.Add("Connection", "keep-alive")
res, err := req.Post(urlStr)
if err != nil {
return ApiPassVerifyCode(client, code, retry-1)
return ApiPassVerifyCode(client, code, retryCount-1)
}
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))
return res.StatusCode() == 302
File diff suppressed because it is too large Load Diff
+41 -124
View File
@@ -2,11 +2,27 @@ package xuexitong
import (
"context"
"likedream-goframe/internal/application/xuexitong/entity"
"likedream-goframe/internal/framework/errors"
"likedream-goframe/internal/utils"
"testing"
)
func TestSchedule(t *testing.T) {
ctx := context.Background()
xuexitongLogin := NewXuexitongLogin(ctx)
err := xuexitongLogin.Login(context.Background(), "13151832241", "yyz@083422", 5)
utils.OkOrPanic(err)
client := xuexitongLogin.GetClient()
courseService := NewXuexitongCourse(client)
// 人人必修的人工智能课
err = courseService.AutoStudyByCourseID(ctx, "258109701")
if err != nil {
utils.OkOrPanic(errors.NewBusinessErrorWithDetail(503, "刷课失败", err))
}
}
func TestXuexitongLogin(t *testing.T) {
xuexitongLogin := NewXuexitongLogin(context.Background())
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
@@ -39,7 +55,7 @@ func TestPullChapter(t *testing.T) {
}
// 获取第一个课程的章节
chapters := courseAction.PullCourseChapterList(context.Background(), courseList[0].CourseID)
chapters := courseAction.PullCourseChapterList(context.Background(), courseList[0])
t.Logf("课程 %s 的章节数量: %d", courseList[0].CourseName, len(chapters))
for _, chapter := range chapters {
@@ -64,147 +80,48 @@ func TestFetchChapterCards(t *testing.T) {
course := courseList[0]
// 获取章节列表
chapters := courseAction.PullCourseChapterList(context.Background(), course.CourseID)
chapters := courseAction.PullCourseChapterList(context.Background(), course)
if len(chapters) == 0 {
t.Fatal("没有找到章节")
}
// 找一个有任务点的章节
var targetChapter *entity.XueXiTCourseChapter
for i := range chapters {
if chapters[i].JobCount > 0 {
targetChapter = &chapters[i]
var targetChapterID int
for _, ch := range chapters {
if ch.JobCount > 0 {
targetChapterID = ch.ID
break
}
}
if targetChapter == nil {
if targetChapterID == 0 {
t.Fatal("没有找到有任务点的章节")
}
// 获取任务点卡片
cards, err := courseAction.FetchChapterCards(context.Background(), course, targetChapter.ID, targetChapter.ID)
cards, points, err := courseAction.FetchChapterCards(context.Background(), course, targetChapterID)
utils.OkOrPanic(err)
t.Logf("章节 %s 的任务点数量: %d", targetChapter.Name, len(cards))
t.Logf("卡片数量: %d", len(cards))
if points != nil {
t.Logf("视频数量: %d, 文档数量: %d, 作业数量: %d, 外链数量: %d, 直播数量: %d",
len(points.Videos), len(points.Documents), len(points.Works),
len(points.Hyperlinks), len(points.Lives))
for _, card := range cards {
t.Logf("任务点: 类型=%s, JobID=%s, ObjectID=%s", card.Type, card.JobID, card.ObjectID)
}
}
func TestAutoPlayVideo(t *testing.T) {
xuexitongLogin := NewXuexitongLogin(context.Background())
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
utils.OkOrPanic(err)
client := xuexitongLogin.GetClient()
courseAction := NewXuexitongCourse(client)
// 获取课程列表
courseList := courseAction.PullCourseList(context.Background())
if len(courseList) == 0 {
t.Fatal("没有找到课程")
}
course := courseList[0]
// 获取章节列表
chapters := courseAction.PullCourseChapterList(context.Background(), course.CourseID)
if len(chapters) == 0 {
t.Fatal("没有找到章节")
}
// 找一个有任务点的章节
var targetChapter *entity.XueXiTCourseChapter
for i := range chapters {
if chapters[i].JobCount > 0 {
targetChapter = &chapters[i]
break
for _, video := range points.Videos {
t.Logf("视频任务点: JobID=%s, ObjectID=%s", video.JobID, video.ObjectID)
}
}
if targetChapter == nil {
t.Fatal("没有找到有任务点的章节")
}
// 获取任务点卡片
cards, err := courseAction.FetchChapterCards(context.Background(), course, targetChapter.ID, targetChapter.ID)
utils.OkOrPanic(err)
// 找视频任务点
for _, card := range cards {
if card.Type == "video" {
t.Logf("开始处理视频任务点: %s", card.JobID)
// 获取视频信息
videoDto, err := courseAction.FetchVideoDto(context.Background(), card.ObjectID, "", card.JobID)
utils.OkOrPanic(err)
// 自动播放视频
err = courseAction.AutoPlayVideo(context.Background(), course, card, videoDto)
utils.OkOrPanic(err)
t.Logf("视频任务点完成: %s", card.JobID)
break // 只测试一个视频
for _, doc := range points.Documents {
t.Logf("文档任务点: JobID=%s, Type=%s", doc.JobID, doc.Type)
}
}
}
func TestAutoReadDocument(t *testing.T) {
xuexitongLogin := NewXuexitongLogin(context.Background())
err := xuexitongLogin.Login(context.Background(), "13551503012", "xiaozhi520", 5)
utils.OkOrPanic(err)
client := xuexitongLogin.GetClient()
courseAction := NewXuexitongCourse(client)
// 获取课程列表
courseList := courseAction.PullCourseList(context.Background())
if len(courseList) == 0 {
t.Fatal("没有找到课程")
}
course := courseList[0]
// 获取章节列表
chapters := courseAction.PullCourseChapterList(context.Background(), course.CourseID)
if len(chapters) == 0 {
t.Fatal("没有找到章节")
}
// 找一个有任务点的章节
var targetChapter *entity.XueXiTCourseChapter
for i := range chapters {
if chapters[i].JobCount > 0 {
targetChapter = &chapters[i]
break
for _, work := range points.Works {
t.Logf("作业任务点: JobID=%s, WorkID=%s", work.JobID, work.WorkID)
}
}
if targetChapter == nil {
t.Fatal("没有找到有任务点的章节")
}
// 获取任务点卡片
cards, err := courseAction.FetchChapterCards(context.Background(), course, targetChapter.ID, targetChapter.ID)
utils.OkOrPanic(err)
// 找文档任务点
for _, card := range cards {
if card.Type == "document" {
t.Logf("开始处理文档任务点: %s", card.JobID)
// 获取文档信息
documentDto, err := courseAction.FetchDocumentDto(context.Background(), card.ObjectID, "", card.JobID)
utils.OkOrPanic(err)
// 自动阅读文档
err = courseAction.AutoReadDocument(context.Background(), course, card, documentDto)
utils.OkOrPanic(err)
t.Logf("文档任务点完成: %s", card.JobID)
break // 只测试一个文档
for _, link := range points.Hyperlinks {
t.Logf("外链任务点: JobID=%s", link.JobID)
}
for _, live := range points.Lives {
t.Logf("直播任务点: JobID=%s, LiveId=%s, Title=%s", live.JobID, live.LiveId, live.Title)
}
}
}
@@ -2,9 +2,9 @@ package xuexitong
import (
"crypto/md5"
crand "crypto/rand"
"encoding/hex"
"fmt"
"math/rand"
"strings"
)
@@ -34,7 +34,7 @@ func GetUA(uaType string) string {
return strings.Join([]string{
fmt.Sprintf("Mozilla/5.0 (Linux; %s; %s Build/OPM1.171019.019; wv)", ANDROID_VERSION, DEVICE_VENDOR),
fmt.Sprintf("AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36"),
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36",
fmt.Sprintf("(schild:%s)", MobileUASign(DEVICE_VENDOR, "zh_CN", APP_VERSION, BUILD, IMEI)),
fmt.Sprintf("(device:%s)", DEVICE_VENDOR),
"Language/zh_CN",
@@ -48,15 +48,14 @@ func GetUA(uaType string) string {
}
}
// 用于生成IMEI
func TokenHex(len int) string {
b := make([]byte, 16)
_, err := rand.Read(b)
// TokenHex 用于生成IMEI
func TokenHex(length int) string {
b := make([]byte, length)
_, err := crand.Read(b)
if err != nil {
panic(err)
}
token := hex.EncodeToString(b)
return token
return hex.EncodeToString(b)
}
func MobileUASign(model, locale, version, build, imei string) string {
+2 -1
View File
@@ -37,7 +37,8 @@ const (
ConfigEmailFromName = "email.fromName"
// 基础积分配置
ConfigBasePointCpdaily = "basePoint.cpdaily"
ConfigBasePointCpdailyTask = "basePoint.cpdailyTask"
ConfigBasePointCpdailyLeave = "basePoint.cpdailyLeave"
)
const (
@@ -5,6 +5,7 @@ import (
"context"
"encoding/gob"
"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"
@@ -105,13 +106,25 @@ func (h *XuexitongTaskHandler) ProcessTask(ctx context.Context, task *asynq.Task
// 刷课完成后,统计任务情况
g.Log().Infof(ctx, "刷课完成,正在统计任务情况...")
// 获取章节列表
chapters := courseService.PullCourseChapterList(ctx, targetCourse.CourseID)
// 先获取课程列表找到目标课程
courseList := courseService.PullCourseList(ctx)
var course *entity.XueXiTCourse
for _, c := range courseList {
if c.CourseID == targetCourse.CourseID {
course = &c
break
}
}
// 统计总任务数(累加所有章节的JobCount)
totalTasks := 0
for _, chapter := range chapters {
totalTasks += chapter.JobCount
if course != nil {
// 获取章节列表
chapters := courseService.PullCourseChapterList(ctx, *course)
// 统计总任务数(累加所有章节的JobCount)
for _, chapter := range chapters {
totalTasks += chapter.JobCount
}
}
g.Log().Infof(ctx, "课程 %s 统计完成: 总任务点 %d", targetCourse.CourseName, totalTasks)
@@ -18,6 +18,9 @@ type BalanceServiceV1 struct {
}
func (v *BalanceServiceV1) ChangeUserBalance(ctx context.Context, userId int64, balance decimal.Decimal, reason string) {
if decimal.Zero.Equal(balance) {
return
}
err := db.WithTx(ctx, func(c context.Context, tx *ent.Tx) {
// 使用 SELECT ... FOR UPDATE 锁定用户记录
user, err := tx.SysUser.Query().
+72 -28
View File
@@ -13,6 +13,7 @@ import (
"likedream-goframe/internal/ent/sysuser"
"likedream-goframe/internal/framework/logger"
"likedream-goframe/internal/utils"
"strconv"
"github.com/bytedance/sonic"
"go.uber.org/zap"
@@ -78,9 +79,19 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
client := loginClient.GetClient()
courseService := xuexitong.NewXuexitongCourse(client)
var targetCourse *entity.XueXiTCourse
// 获取课程章节列表
chapters := courseService.PullCourseChapterList(ctx, req.CourseID)
courseList := courseService.PullCourseList(ctx)
for _, course := range courseList {
if course.CourseID == req.CourseID {
targetCourse = &course
break
}
}
if targetCourse == nil {
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
}
chapters := courseService.PullCourseChapterList(ctx, *targetCourse)
// 定义节点信息结构
type NodeInfo struct {
@@ -94,46 +105,79 @@ func (s CourseServiceV1) AddCourse(ctx context.Context, req *v1.AddCourseReq) *u
var allNodes []NodeInfo
var completedNodes []string
// 获取课程信息用于API调用
courses := courseService.PullCourseList(ctx)
var targetCourse *entity.XueXiTCourse
for _, c := range courses {
if c.CourseID == req.CourseID {
targetCourse = &c
break
}
}
if targetCourse == nil {
utils.OkOrPanic(errors2.NewBusinessError(400, "未找到该课程"))
}
// 递归获取所有章节的任务点
var collectNodes func(chapters []entity.XueXiTCourseChapter)
collectNodes = func(chapters []entity.XueXiTCourseChapter) {
for _, chapter := range chapters {
// 如果有任务点,获取卡片信息
if chapter.JobCount > 0 {
cards, err := courseService.FetchChapterCards(ctx, *targetCourse, chapter.ID, chapter.ID)
cards, points, err := courseService.FetchChapterCards(ctx, *targetCourse, chapter.ID)
if err != nil {
logger.Logger.Warn("获取章节任务点失败", zap.String("chapter", chapter.Name), zap.Error(err))
} else {
for _, card := range cards {
} else if points != nil {
chapterIDStr := strconv.Itoa(chapter.ID)
// 统计视频任务点
for _, video := range points.Videos {
nodeInfo := NodeInfo{
ChapterID: chapter.ID,
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: card.ID,
CardTitle: card.Title,
CardType: card.Type,
CardID: video.JobID,
CardTitle: video.Title,
CardType: "video",
}
allNodes = append(allNodes, nodeInfo)
// 检查是否已完成
if card.IsPassed {
nodeKey := chapter.ID + ":" + card.ID
completedNodes = append(completedNodes, nodeKey)
if video.IsPassed {
completedNodes = append(completedNodes, chapterIDStr+":"+video.JobID)
}
}
// 统计文档任务点
for _, doc := range points.Documents {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: doc.JobID,
CardTitle: doc.Title,
CardType: "document",
}
allNodes = append(allNodes, nodeInfo)
}
// 统计作业任务点
for _, work := range points.Works {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: work.JobID,
CardTitle: work.Title,
CardType: "work",
}
allNodes = append(allNodes, nodeInfo)
if work.IsPassed {
completedNodes = append(completedNodes, chapterIDStr+":"+work.JobID)
}
}
// 统计外链任务点
for _, link := range points.Hyperlinks {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: link.JobID,
CardTitle: link.Title,
CardType: "hyperlink",
}
allNodes = append(allNodes, nodeInfo)
}
// 统计直播任务点
for _, live := range points.Lives {
nodeInfo := NodeInfo{
ChapterID: chapterIDStr,
ChapterName: chapter.Name,
CardID: live.JobID,
CardTitle: live.Title,
CardType: "live",
}
allNodes = append(allNodes, nodeInfo)
}
_ = cards // cards 暂时不使用
}
}
+2 -2
View File
@@ -120,14 +120,14 @@ func (v CpLeaveServiceV1) Report(ctx context.Context, req *v1.CpLeaveReportReq)
}
// 计算扣款金额
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdaily).String())
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdailyLeave).String())
utils.OkOrPanic(err)
if user.Edges.Agent != nil {
basePoint = basePoint.Mul(user.Edges.Agent.Rate)
}
// 先扣款
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "销假")
balance.BalanceService.ChangeUserBalance(ctx, user.ID, basePoint.Neg(), "今日校园销假")
// 创建销假记录
leaveRecord := db2.Global.CpLeave.Create().
+1 -1
View File
@@ -180,7 +180,7 @@ func (v CpTaskServiceV1) PostExecute(ctx context.Context, req *v1.PostCpTaskExec
}
//扣积分
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdaily).String())
basePoint, err := decimal.NewFromString(g.Cfg().MustGet(ctx, cst2.ConfigBasePointCpdailyTask).String())
utils.OkOrPanic(err)
if user.Edges.Agent != nil {
basePoint = basePoint.Mul(user.Edges.Agent.Rate)