760 lines
23 KiB
Go
760 lines
23 KiB
Go
package xuexitong
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"likedream-goframe/internal/application/captcha"
|
|
"likedream-goframe/internal/application/xuexitong/entity"
|
|
errors2 "likedream-goframe/internal/common/errors"
|
|
"likedream-goframe/internal/framework/logger"
|
|
"likedream-goframe/internal/utils"
|
|
"net/url"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/avast/retry-go/v4"
|
|
"github.com/samber/oops"
|
|
"go.uber.org/zap"
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
// PullCourseChapterList 拉取学习通课程章节列表并返回
|
|
func (c *XuexitongCourse) PullCourseChapterList(ctx context.Context, course entity.XueXiTCourse) []entity.XueXiTCourseChapter {
|
|
var chapterString string
|
|
key, _ := strconv.Atoi(course.Key)
|
|
|
|
e := retry.Do(func() error {
|
|
params := url.Values{}
|
|
params.Add("id", strconv.Itoa(key))
|
|
params.Add("personid", strconv.Itoa(course.Cpi))
|
|
params.Add("fields", "id,bbsid,classscore,isstart,allowdownload,chatid,name,state,isfiled,visiblescore,hideclazz,begindate,forbidintoclazz,coursesetting.fields(id,courseid,hiddencoursecover,coursefacecheck),course.fields(id,belongschoolid,name,infocontent,objectid,app,bulletformat,mappingcourseid,imageurl,teacherfactor,jobcount,knowledge.fields(id,name,indexOrder,parentnodeid,status,isReview,layer,label,jobcount,begintime,endtime,attachment.fields(id,type,objectid,extension).type(video)))")
|
|
params.Add("view", "json")
|
|
|
|
get, err := c.client.R().SetHeaders(map[string]string{
|
|
"User-Agent": GetUA("mobile"),
|
|
"Accept-Language": "zh_CN",
|
|
"Host": "mooc1-api.chaoxing.com",
|
|
"Connection": "Keep-Alive",
|
|
}).Get(ApiPullChapter + "?" + params.Encode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
chapterString = get.String()
|
|
return nil
|
|
}, retry.Attempts(3), retry.Delay(5*time.Second))
|
|
utils.OkOrPanic(e, oops.WithContext(ctx).With("chapterString", chapterString).Wrap)
|
|
|
|
var chapterMap map[string]interface{}
|
|
err := json.Unmarshal([]byte(chapterString), &chapterMap)
|
|
if err != nil {
|
|
utils.OkOrPanic(errors2.NewBusinessError(503, "解析章节数据失败").WithInnerError(err), oops.WithContext(ctx).Wrap)
|
|
}
|
|
|
|
dataArr, ok := chapterMap["data"].([]interface{})
|
|
if !ok || len(dataArr) == 0 {
|
|
return nil
|
|
}
|
|
|
|
firstData, ok := dataArr[0].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
courseData, ok := firstData["course"].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
courseDataArr, ok := courseData["data"].([]interface{})
|
|
if !ok || len(courseDataArr) == 0 {
|
|
return nil
|
|
}
|
|
|
|
firstCourse, ok := courseDataArr[0].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
knowledgeData, ok := firstCourse["knowledge"].(map[string]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
knowledgeArr, ok := knowledgeData["data"].([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
var chapters []entity.XueXiTCourseChapter
|
|
for _, kd := range knowledgeArr {
|
|
kdMap, ok := kd.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
chapter := entity.XueXiTCourseChapter{
|
|
ID: int(kdMap["id"].(float64)),
|
|
Name: kdMap["name"].(string),
|
|
ParentNodeID: int(kdMap["parentnodeid"].(float64)),
|
|
Layer: int(kdMap["layer"].(float64)),
|
|
Label: kdMap["label"].(string),
|
|
IndexOrder: int(kdMap["indexorder"].(float64)),
|
|
JobCount: int(kdMap["jobcount"].(float64)),
|
|
Status: kdMap["status"].(string),
|
|
IsReview: int(kdMap["isreview"].(float64)),
|
|
}
|
|
chapters = append(chapters, chapter)
|
|
}
|
|
|
|
sort.Slice(chapters, func(i, j int) bool {
|
|
iLabelParts := strings.Split(chapters[i].Label, ".")
|
|
jLabelParts := strings.Split(chapters[j].Label, ".")
|
|
for k := range iLabelParts {
|
|
if k >= len(jLabelParts) {
|
|
return false
|
|
}
|
|
iv, _ := strconv.Atoi(iLabelParts[k])
|
|
jv, _ := strconv.Atoi(jLabelParts[k])
|
|
if iv != jv {
|
|
return iv < jv
|
|
}
|
|
}
|
|
return len(iLabelParts) < len(jLabelParts)
|
|
})
|
|
|
|
return chapters
|
|
}
|
|
|
|
// FetchChapterPointStatus 获取章节任务点状态
|
|
func (c *XuexitongCourse) FetchChapterPointStatus(ctx context.Context, course entity.XueXiTCourse, chapters []entity.XueXiTCourseChapter) (entity.ChapterPointStatus, error) {
|
|
var nodes []string
|
|
for _, ch := range chapters {
|
|
nodes = append(nodes, strconv.Itoa(ch.ID))
|
|
}
|
|
|
|
ts := time.Now().UnixMilli()
|
|
classID, _ := strconv.Atoi(course.Key)
|
|
courseID, _ := strconv.Atoi(course.CourseID)
|
|
|
|
values := url.Values{
|
|
"view": {"json"},
|
|
"nodes": {strings.Join(nodes, ",")},
|
|
"clazzid": {strconv.Itoa(classID)},
|
|
"time": {strconv.FormatInt(ts, 10)},
|
|
"userid": {c.userID},
|
|
"cpi": {strconv.Itoa(course.Cpi)},
|
|
"courseid": {strconv.Itoa(courseID)},
|
|
}
|
|
|
|
logger.Logger.Debug("FetchChapterPointStatus 请求参数",
|
|
zap.String("userID", c.userID),
|
|
zap.Int("classID", classID),
|
|
zap.Int("courseID", courseID),
|
|
zap.Int("cpi", course.Cpi),
|
|
zap.Int("nodesCount", len(nodes)))
|
|
|
|
var body string
|
|
err := retry.Do(func() error {
|
|
resp, err := c.client.R().SetHeaders(map[string]string{
|
|
"User-Agent": GetUA("mobile"),
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
}).SetBody(values.Encode()).Post(ApiChapterPoint)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = resp.String()
|
|
return nil
|
|
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.Logger.Debug("FetchChapterPointStatus 响应", zap.String("body", body))
|
|
|
|
var status entity.ChapterPointStatus
|
|
err = json.Unmarshal([]byte(body), &status)
|
|
if err != nil {
|
|
logger.Logger.Error("FetchChapterPointStatus 解析失败", zap.Error(err), zap.String("body", body))
|
|
return nil, err
|
|
}
|
|
|
|
for i := range chapters {
|
|
chapterIDStr := strconv.Itoa(chapters[i].ID)
|
|
if pointData, exists := status[chapterIDStr]; exists {
|
|
chapters[i].PointTotal = pointData.UnFinishCount + pointData.FinishCount
|
|
chapters[i].PointFinish = pointData.FinishCount
|
|
}
|
|
}
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// EnterChapterForwardCall 进入章节前的前置调用
|
|
func (c *XuexitongCourse) EnterChapterForwardCall(ctx context.Context, course entity.XueXiTCourse, chapterID int) error {
|
|
urlStr := fmt.Sprintf("%s?courseId=%s&clazzid=%s&chapterId=%d&cpi=%d&verificationcode=&mooc2=1&toComputer=falseµTopicId=0",
|
|
ApiEnterChapter, course.CourseID, course.Key, chapterID, course.Cpi)
|
|
_, err := c.client.R().SetHeaders(map[string]string{
|
|
"User-Agent": GetUA("mobile"),
|
|
}).Get(urlStr)
|
|
|
|
return err
|
|
}
|
|
|
|
// FetchChapterCards 获取章节任务点卡片(JSON方式)- 支持所有任务点类型
|
|
func (c *XuexitongCourse) FetchChapterCards(ctx context.Context, course entity.XueXiTCourse, chapterID int) ([]entity.Card, []entity.ChapterPoint, error) {
|
|
var body string
|
|
courseID, _ := strconv.Atoi(course.CourseID)
|
|
|
|
err := retry.Do(func() error {
|
|
resp, err := c.client.R().SetHeaders(map[string]string{
|
|
"User-Agent": GetUA("mobile"),
|
|
"Accept-Language": "zh_CN",
|
|
}).SetQueryParams(map[string]string{
|
|
"id": strconv.Itoa(chapterID),
|
|
"courseid": strconv.Itoa(courseID),
|
|
"fields": "id,parentnodeid,indexorder,label,layer,name,begintime,createtime,lastmodifytime,status,jobUnfinishedCount,clickcount,openlock,card.fields(id,knowledgeid,title,knowledgeTitile,description,cardorder).contentcard(all)",
|
|
"view": "json",
|
|
"token": "4faa8662c59590c6f43ae9fe5b002b42",
|
|
"_time": strconv.FormatInt(time.Now().UnixMilli(), 10),
|
|
}).Get(ApiChapterCards)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
body = resp.String()
|
|
if strings.Contains(body, "请输入验证码") {
|
|
logger.Logger.Warn("触发验证码,正在进行AI智能识别绕过.....")
|
|
e := retry.Do(func() error {
|
|
bytes, e := ApiVerifyCodeBytes(c.client)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
codeResult := captcha.ApiSemiOCRVerification(bytes, 1, 23)
|
|
time.Sleep(5 * time.Second)
|
|
if ApiPassVerifyCode(c.client, codeResult, 5) {
|
|
return nil
|
|
}
|
|
return errors2.NewBusinessError(503, "验证码识别失败")
|
|
}, retry.Attempts(5), retry.Delay(5*time.Second))
|
|
return e
|
|
}
|
|
|
|
statusCode := resp.StatusCode()
|
|
if statusCode == 500 || statusCode == 202 || statusCode == 400 || statusCode == 403 {
|
|
return errors2.NewBusinessError(statusCode, fmt.Sprintf("获取任务点失败,状态码: %d", statusCode))
|
|
}
|
|
return nil
|
|
}, retry.Attempts(3), retry.Delay(5*time.Second))
|
|
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
var apiResp entity.CardAPIResponse
|
|
if err := json.Unmarshal([]byte(body), &apiResp); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if len(apiResp.Data) == 0 {
|
|
return nil, nil, nil
|
|
}
|
|
|
|
dataItem := apiResp.Data[0]
|
|
cards := dataItem.Card.Data
|
|
|
|
var points []entity.ChapterPoint
|
|
cpiStr := strconv.Itoa(course.Cpi)
|
|
|
|
for cardIndex, card := range cards {
|
|
logger.Logger.Debug("处理卡片",
|
|
zap.Int("cardIndex", cardIndex),
|
|
zap.Int("cardID", card.ID),
|
|
zap.Int("cardKnowledgeID", card.KnowledgeID),
|
|
zap.String("cardTitle", card.Title),
|
|
zap.Int("descriptionLength", len(card.Description)))
|
|
|
|
if card.Description == "" {
|
|
continue
|
|
}
|
|
iframes, err := c.parseIframeData(card.Description)
|
|
if err != nil {
|
|
logger.Logger.Debug("解析卡片失败", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
for _, iframe := range iframes {
|
|
if !iframe.HasData {
|
|
continue
|
|
}
|
|
pointType, ok := iframe.Other["module"]
|
|
logger.Logger.Debug("解析到任务点", zap.String("module", pointType), zap.Bool("hasModule", ok), zap.Any("data", iframe.Data))
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
point := entity.ChapterPoint{
|
|
CardIndex: cardIndex,
|
|
KnowledgeID: card.KnowledgeID,
|
|
CourseID: course.CourseID,
|
|
ClassID: course.Key,
|
|
Cpi: cpiStr,
|
|
Title: card.Title,
|
|
}
|
|
|
|
switch pointType {
|
|
case "insertvideo", "insertaudio":
|
|
objectID, ok := iframe.Data["objectid"].(string)
|
|
if !ok || objectID == "" {
|
|
continue
|
|
}
|
|
jobID, _ := iframe.Data["_jobid"].(string)
|
|
point.Type = entity.ChapterPointTypeVideo
|
|
point.ObjectID = objectID
|
|
point.JobID = jobID
|
|
|
|
cords2, err := c.FetchChapterCards2(ctx, course, card.KnowledgeID)
|
|
if err == nil && cords2 != nil {
|
|
c.fillVideoPointFromCards2(cords2, &point, objectID)
|
|
}
|
|
|
|
case "insertdoc", "document":
|
|
objectID, _ := iframe.Data["objectid"].(string)
|
|
jobID, _ := iframe.Data["_jobid"].(string)
|
|
if jobID == "" {
|
|
continue
|
|
}
|
|
point.Type = entity.ChapterPointTypeDocument
|
|
point.ObjectID = objectID
|
|
point.JobID = jobID
|
|
point.DocumentType = "document"
|
|
|
|
case "insertbook":
|
|
jobID, _ := iframe.Data["_jobid"].(string)
|
|
if jobID == "" {
|
|
continue
|
|
}
|
|
point.Type = entity.ChapterPointTypeDocument
|
|
point.JobID = jobID
|
|
point.DocumentType = "insertbook"
|
|
|
|
case "work", "insertwork":
|
|
workID, _ := iframe.Data["workid"].(string)
|
|
schoolID, _ := iframe.Data["schoolid"].(string)
|
|
jobID, _ := iframe.Data["_jobid"].(string)
|
|
if workID == "" || jobID == "" {
|
|
continue
|
|
}
|
|
if schoolID == "" {
|
|
schoolID = "0"
|
|
}
|
|
point.Type = entity.ChapterPointTypeWork
|
|
point.WorkID = workID
|
|
point.SchoolID = schoolID
|
|
point.JobID = jobID
|
|
|
|
case "hyperlink", "insertlink":
|
|
jobID, _ := iframe.Data["_jobid"].(string)
|
|
if jobID == "" {
|
|
continue
|
|
}
|
|
linkType := 0
|
|
if lt, ok := iframe.Data["linkType"].(float64); ok {
|
|
linkType = int(lt)
|
|
}
|
|
point.Type = entity.ChapterPointTypeHyperlink
|
|
point.JobID = jobID
|
|
point.LinkType = linkType
|
|
|
|
case "insertlive":
|
|
jobID, _ := iframe.Data["_jobid"].(string)
|
|
if jobID == "" {
|
|
continue
|
|
}
|
|
title, _ := iframe.Data["title"].(string)
|
|
liveStatus, _ := iframe.Data["liveStatus"].(string)
|
|
streamName, _ := iframe.Data["streamName"].(string)
|
|
vdoid, _ := iframe.Data["vdoid"].(string)
|
|
liveId := ""
|
|
if lid, ok := iframe.Data["liveId"].(float64); ok {
|
|
liveId = strconv.FormatInt(int64(lid), 10)
|
|
}
|
|
live, _ := iframe.Data["live"].(bool)
|
|
|
|
point.Type = entity.ChapterPointTypeLive
|
|
point.JobID = jobID
|
|
point.Title = title
|
|
point.LiveStatusStr = liveStatus
|
|
point.StreamName = streamName
|
|
point.Vdoid = vdoid
|
|
point.LiveId = liveId
|
|
point.Live = live
|
|
point.Module = pointType
|
|
point.UserId = c.userID
|
|
|
|
default:
|
|
logger.Logger.Debug("未知的任务点类型", zap.String("type", pointType))
|
|
continue
|
|
}
|
|
|
|
points = append(points, point)
|
|
}
|
|
}
|
|
|
|
return cards, points, nil
|
|
}
|
|
|
|
// fillVideoPointFromCards2 从 FetchChapterCards2 结果填充视频任务点参数
|
|
func (c *XuexitongCourse) fillVideoPointFromCards2(cords2 map[string]interface{}, point *entity.ChapterPoint, objectID string) {
|
|
if attachments, ok := cords2["attachments"].([]interface{}); ok {
|
|
for _, att := range attachments {
|
|
if attMap, ok := att.(map[string]interface{}); ok {
|
|
attObjectID, _ := attMap["objectId"].(string)
|
|
if attObjectID == "" {
|
|
if prop, ok := attMap["property"].(map[string]interface{}); ok {
|
|
attObjectID, _ = prop["objectid"].(string)
|
|
}
|
|
}
|
|
if attObjectID == objectID {
|
|
if otherInfo, ok := attMap["otherInfo"].(string); ok && len(otherInfo) > 80 {
|
|
parts := strings.SplitN(otherInfo, "&", 2)
|
|
if len(parts) > 0 {
|
|
point.OtherInfo = parts[0]
|
|
}
|
|
}
|
|
if jid, ok := attMap["jobid"].(string); ok && jid != "" {
|
|
point.JobID = jid
|
|
}
|
|
if playTime, ok := attMap["playTime"].(float64); ok {
|
|
point.PlayTime = int(playTime) / 1000
|
|
}
|
|
if ade, ok := attMap["attDurationEnc"].(string); ok {
|
|
point.AttDurationEnc = ade
|
|
}
|
|
if vfce, ok := attMap["videoFaceCaptureEnc"].(string); ok {
|
|
point.VideoFaceCaptureEnc = vfce
|
|
}
|
|
if isPassed, ok := attMap["isPassed"].(bool); ok {
|
|
point.IsPassed = isPassed
|
|
}
|
|
point.RT = 0.9
|
|
if prop, ok := attMap["property"].(map[string]interface{}); ok {
|
|
if rtObj, ok := prop["rt"]; ok {
|
|
switch v := rtObj.(type) {
|
|
case float64:
|
|
point.RT = v
|
|
case string:
|
|
if rt, err := strconv.ParseFloat(v, 64); err == nil {
|
|
point.RT = rt
|
|
}
|
|
}
|
|
}
|
|
}
|
|
logger.Logger.Debug("从 FetchChapterCards2 获取到视频参数",
|
|
zap.String("objectID", objectID),
|
|
zap.String("otherInfo", point.OtherInfo),
|
|
zap.String("attDurationEnc", point.AttDurationEnc),
|
|
zap.String("videoFaceCaptureEnc", point.VideoFaceCaptureEnc),
|
|
zap.Float64("RT", point.RT))
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if defaults, ok := cords2["defaults"].(map[string]interface{}); ok {
|
|
if fid, ok := defaults["fid"].(string); ok {
|
|
point.FID, _ = strconv.Atoi(fid)
|
|
}
|
|
if cpiVal, ok := defaults["cpi"]; ok {
|
|
switch v := cpiVal.(type) {
|
|
case float64:
|
|
point.Cpi = strconv.FormatInt(int64(v), 10)
|
|
case string:
|
|
point.Cpi = v
|
|
}
|
|
}
|
|
if userid, ok := defaults["userid"].(string); ok {
|
|
c.userID = userid
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseIframeData 从HTML中解析iframe数据
|
|
func (c *XuexitongCourse) parseIframeData(htmlString string) ([]entity.IframeAttributes, error) {
|
|
node, err := html.Parse(strings.NewReader(htmlString))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var iframes []entity.IframeAttributes
|
|
var traverse func(n *html.Node)
|
|
traverse = func(n *html.Node) {
|
|
if n.Type == html.ElementNode && n.Data == "iframe" {
|
|
attrs := entity.IframeAttributes{
|
|
Other: make(map[string]string),
|
|
}
|
|
hasData := false
|
|
for _, attr := range n.Attr {
|
|
if attr.Key == "data" && strings.TrimSpace(attr.Val) != "" {
|
|
hasData = true
|
|
cleanedData := strings.ReplaceAll(attr.Val, """, "\"")
|
|
cleanedData = regexp.MustCompile(`\s+`).ReplaceAllString(cleanedData, "")
|
|
if err := json.Unmarshal([]byte(cleanedData), &attrs.Data); err != nil {
|
|
logger.Logger.Debug("Failed to decode JSON", zap.Error(err))
|
|
}
|
|
} else {
|
|
attrs.Other[attr.Key] = attr.Val
|
|
}
|
|
}
|
|
attrs.HasData = hasData
|
|
iframes = append(iframes, attrs)
|
|
}
|
|
for ch := n.FirstChild; ch != nil; ch = ch.NextSibling {
|
|
traverse(ch)
|
|
}
|
|
}
|
|
traverse(node)
|
|
return iframes, nil
|
|
}
|
|
|
|
// 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)
|
|
|
|
logger.Logger.Debug("FetchChapterCards2 请求",
|
|
zap.String("url", urlStr),
|
|
zap.Int("knowledgeID", knowledgeID))
|
|
|
|
var body string
|
|
err := retry.Do(func() error {
|
|
resp, err := c.client.R().SetHeaders(map[string]string{
|
|
"User-Agent": GetUA("mobile"),
|
|
}).Get(urlStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = resp.String()
|
|
return nil
|
|
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
|
|
|
if err != nil {
|
|
logger.Logger.Error("FetchChapterCards2 请求失败", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
logger.Logger.Debug("FetchChapterCards2 响应",
|
|
zap.Int("bodyLength", len(body)),
|
|
zap.Bool("containsMArg", strings.Contains(body, "mArg")))
|
|
|
|
re := regexp.MustCompile(`mArg = ([^;]{6,})`)
|
|
matches := re.FindStringSubmatch(body)
|
|
if len(matches) <= 1 {
|
|
logger.Logger.Warn("FetchChapterCards2 未找到 mArg")
|
|
return nil, nil
|
|
}
|
|
|
|
logger.Logger.Debug("FetchChapterCards2 提取到 mArg", zap.Int("jsonLength", len(matches[1])))
|
|
|
|
var result map[string]interface{}
|
|
if err := json.Unmarshal([]byte(matches[1]), &result); err != nil {
|
|
logger.Logger.Error("FetchChapterCards2 解析 JSON 失败", zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
if attachments, ok := result["attachments"].([]interface{}); ok {
|
|
logger.Logger.Debug("FetchChapterCards2 解析成功",
|
|
zap.Int("attachmentsCount", len(attachments)))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// PageMobileChapterCard 获取章节卡片页面(SSR方式,用于获取附件信息)
|
|
func (c *XuexitongCourse) PageMobileChapterCard(ctx context.Context, course entity.XueXiTCourse, knowledgeID, cardIndex int) (*entity.AttachmentSetting, string, error) {
|
|
classID, _ := strconv.Atoi(course.Key)
|
|
courseID, _ := strconv.Atoi(course.CourseID)
|
|
|
|
params := url.Values{}
|
|
params.Add("clazzid", strconv.Itoa(classID))
|
|
params.Add("courseid", strconv.Itoa(courseID))
|
|
params.Add("knowledgeid", strconv.Itoa(knowledgeID))
|
|
params.Add("num", strconv.Itoa(cardIndex))
|
|
params.Add("isPhone", "1")
|
|
params.Add("control", "true")
|
|
params.Add("cpi", strconv.Itoa(course.Cpi))
|
|
|
|
requestURL := PageChapterCard + "?" + params.Encode()
|
|
logger.Logger.Debug("PageMobileChapterCard 请求",
|
|
zap.String("url", requestURL),
|
|
zap.Int("knowledgeID", knowledgeID),
|
|
zap.Int("cardIndex", cardIndex))
|
|
|
|
var body string
|
|
err := retry.Do(func() error {
|
|
resp, err := c.client.R().SetHeaders(map[string]string{
|
|
"User-Agent": GetUA("mobile"),
|
|
}).Get(requestURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = resp.String()
|
|
return nil
|
|
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
|
|
|
if err != nil {
|
|
logger.Logger.Error("PageMobileChapterCard 请求失败", zap.Error(err))
|
|
return nil, "", err
|
|
}
|
|
|
|
logger.Logger.Debug("PageMobileChapterCard 响应",
|
|
zap.Int("bodyLength", len(body)),
|
|
zap.Bool("containsAttachmentSetting", strings.Contains(body, "window.AttachmentSetting")),
|
|
zap.Bool("containsBlankTips", strings.Contains(body, "blankTips")),
|
|
zap.String("bodyPreview", body[:min(len(body), 500)]))
|
|
|
|
doc, err := html.Parse(strings.NewReader(body))
|
|
if err != nil {
|
|
logger.Logger.Error("解析 HTML 失败", zap.Error(err))
|
|
return nil, "", err
|
|
}
|
|
|
|
var scriptContent string
|
|
var scriptCount int
|
|
var traverse func(*html.Node)
|
|
traverse = func(n *html.Node) {
|
|
if scriptContent != "" {
|
|
return
|
|
}
|
|
if n.Type == html.ElementNode && n.Data == "script" {
|
|
hasTypeAttr := false
|
|
for _, attr := range n.Attr {
|
|
if attr.Key == "type" && strings.TrimSpace(attr.Val) == "text/javascript" {
|
|
hasTypeAttr = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if hasTypeAttr {
|
|
scriptCount++
|
|
if n.FirstChild != nil && n.FirstChild.Type == html.TextNode {
|
|
scriptContent = strings.TrimSpace(n.FirstChild.Data)
|
|
logger.Logger.Debug("找到 script 标签",
|
|
zap.Int("scriptIndex", scriptCount),
|
|
zap.Int("contentLength", len(scriptContent)),
|
|
zap.Bool("hasAttachmentSetting", strings.Contains(scriptContent, "window.AttachmentSetting")))
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
for ch := n.FirstChild; ch != nil; ch = ch.NextSibling {
|
|
traverse(ch)
|
|
}
|
|
}
|
|
traverse(doc)
|
|
|
|
logger.Logger.Debug("HTML 解析完成", zap.Int("scriptCount", scriptCount), zap.Bool("hasScriptContent", scriptContent != ""))
|
|
|
|
if scriptContent == "" || !strings.Contains(scriptContent, "window.AttachmentSetting") {
|
|
var blankTips string
|
|
traverse = func(n *html.Node) {
|
|
if n.Type == html.ElementNode && n.Data == "p" {
|
|
for _, attr := range n.Attr {
|
|
if attr.Key == "class" && strings.Contains(attr.Val, "blankTips") {
|
|
if n.FirstChild != nil {
|
|
blankTips = strings.TrimSpace(n.FirstChild.Data)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
traverse(c)
|
|
}
|
|
}
|
|
traverse(doc)
|
|
|
|
if blankTips != "" {
|
|
logger.Logger.Warn("章节提示信息", zap.String("blankTips", blankTips))
|
|
}
|
|
|
|
if strings.Contains(body, "window.AttachmentSetting") {
|
|
scriptContent = body
|
|
logger.Logger.Debug("从原始 body 中找到 window.AttachmentSetting")
|
|
} else {
|
|
logger.Logger.Warn("未找到 window.AttachmentSetting",
|
|
zap.Int("knowledgeID", knowledgeID),
|
|
zap.Int("cardIndex", cardIndex),
|
|
zap.String("blankTips", blankTips))
|
|
return nil, "", nil
|
|
}
|
|
}
|
|
|
|
re := regexp.MustCompile(`window\.AttachmentSetting\s*=\s*(\{.*?\});`)
|
|
matches := re.FindStringSubmatch(scriptContent)
|
|
var jsonStr string
|
|
if len(matches) > 1 {
|
|
jsonStr = matches[1]
|
|
logger.Logger.Debug("正则提取 JSON 成功", zap.Int("jsonLength", len(jsonStr)))
|
|
} else {
|
|
jsonStr = extractJSONObject(scriptContent, "window.AttachmentSetting")
|
|
if jsonStr == "" {
|
|
logger.Logger.Warn("无法提取 JSON",
|
|
zap.Int("scriptContentLength", len(scriptContent)))
|
|
return nil, "", nil
|
|
}
|
|
logger.Logger.Debug("括号匹配提取 JSON 成功", zap.Int("jsonLength", len(jsonStr)))
|
|
}
|
|
|
|
var attachment entity.AttachmentSetting
|
|
if err := json.Unmarshal([]byte(jsonStr), &attachment); err != nil {
|
|
logger.Logger.Error("解析 AttachmentSetting 失败",
|
|
zap.Error(err),
|
|
zap.String("jsonStr", jsonStr[:min(len(jsonStr), 500)]))
|
|
return nil, "", err
|
|
}
|
|
|
|
logger.Logger.Debug("解析 AttachmentSetting 成功",
|
|
zap.Int("attachmentsCount", len(attachment.Attachments)),
|
|
zap.Any("defaults", attachment.Defaults))
|
|
|
|
for idx, att := range attachment.Attachments {
|
|
property, _ := att["property"].(map[string]interface{})
|
|
objectid := ""
|
|
workid := ""
|
|
if property != nil {
|
|
if oid, ok := property["objectid"].(string); ok {
|
|
objectid = oid
|
|
}
|
|
if wid, ok := property["workid"].(string); ok {
|
|
workid = wid
|
|
}
|
|
}
|
|
otherInfo, _ := att["otherInfo"].(string)
|
|
attDurationEnc, _ := att["attDurationEnc"].(string)
|
|
videoFaceCaptureEnc, _ := att["videoFaceCaptureEnc"].(string)
|
|
enc, _ := att["enc"].(string)
|
|
logger.Logger.Debug("附件信息",
|
|
zap.Int("index", idx),
|
|
zap.String("objectid", objectid),
|
|
zap.String("workid", workid),
|
|
zap.String("enc", enc),
|
|
zap.Bool("hasOtherInfo", otherInfo != ""),
|
|
zap.Bool("hasAttDurationEnc", attDurationEnc != ""),
|
|
zap.Bool("hasVideoFaceCaptureEnc", videoFaceCaptureEnc != ""))
|
|
}
|
|
|
|
reEnc := regexp.MustCompile(`<input type="hidden" id="from" value="[^_]+_[^_]+_[^_]+_([^"]+)"/>`)
|
|
matchesEnc := reEnc.FindStringSubmatch(body)
|
|
enc := ""
|
|
if len(matchesEnc) > 1 {
|
|
enc = matchesEnc[1]
|
|
}
|
|
|
|
return &attachment, enc, nil
|
|
}
|