456 lines
12 KiB
Go
456 lines
12 KiB
Go
package xuexitong
|
||
|
||
import (
|
||
"context"
|
||
"crypto/md5"
|
||
"encoding/hex"
|
||
"encoding/json"
|
||
"fmt"
|
||
"likedream-goframe/internal/application/xuexitong/entity"
|
||
errors2 "likedream-goframe/internal/common/errors"
|
||
"likedream-goframe/internal/framework/logger"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/avast/retry-go/v4"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// VideoDtoFetch 获取视频详细信息
|
||
func (c *XuexitongCourse) VideoDtoFetch(ctx context.Context, p *entity.PointVideoDto) error {
|
||
params := url.Values{}
|
||
params.Set("k", strconv.Itoa(p.FID))
|
||
params.Set("flag", "normal")
|
||
params.Set("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||
|
||
var body string
|
||
err := retry.Do(func() error {
|
||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||
"User-Agent": GetUA("mobile"),
|
||
}).Get(fmt.Sprintf("%s/%s?%s", ApiVideoStatus, p.ObjectID, params.Encode()))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if resp.StatusCode() != 200 {
|
||
return errors2.NewBusinessError(resp.StatusCode(), "获取视频信息失败")
|
||
}
|
||
body = resp.String()
|
||
return nil
|
||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var result map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
||
return err
|
||
}
|
||
|
||
if dtoken, ok := result["dtoken"].(string); ok {
|
||
p.DToken = dtoken
|
||
}
|
||
if duration, ok := result["duration"].(float64); ok {
|
||
p.Duration = int(duration)
|
||
}
|
||
if filename, ok := result["filename"].(string); ok {
|
||
if decoded, err := url.QueryUnescape(filename); err == nil {
|
||
p.Title = decoded
|
||
} else {
|
||
p.Title = filename
|
||
}
|
||
}
|
||
if status, ok := result["status"].(string); ok && status == "success" {
|
||
return nil
|
||
}
|
||
|
||
return errors2.NewBusinessError(503, "视频状态异常")
|
||
}
|
||
|
||
// AttachmentsDetectionVideo 从附件设置中检测并填充视频信息(旧版,保留兼容)
|
||
func (c *XuexitongCourse) AttachmentsDetectionVideo(attachment *entity.AttachmentSetting, p *entity.PointVideoDto) error {
|
||
if attachment == nil {
|
||
return errors2.NewBusinessError(503, "附件设置为空")
|
||
}
|
||
|
||
for _, att := range attachment.Attachments {
|
||
property, ok := att["property"].(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
objectid, _ := property["objectid"].(string)
|
||
if objectid != p.ObjectID {
|
||
continue
|
||
}
|
||
|
||
if jobid, ok := property["jobid"].(string); ok {
|
||
p.JobID = jobid
|
||
} else if jobidFloat, ok := property["jobid"].(float64); ok {
|
||
p.JobID = strconv.FormatFloat(jobidFloat, 'f', -1, 64)
|
||
}
|
||
|
||
if otherInfo, ok := att["otherInfo"].(string); ok {
|
||
parts := strings.SplitN(otherInfo, "&", 2)
|
||
if len(parts) > 0 {
|
||
p.OtherInfo = parts[0]
|
||
}
|
||
}
|
||
|
||
if isPassed, ok := att["isPassed"].(bool); ok {
|
||
p.IsPassed = isPassed
|
||
}
|
||
|
||
if rtObj, ok := property["rt"]; ok {
|
||
switch v := rtObj.(type) {
|
||
case float64:
|
||
p.RT = v
|
||
case string:
|
||
if rt, err := strconv.ParseFloat(v, 64); err == nil {
|
||
p.RT = rt
|
||
} else {
|
||
p.RT = 0.9
|
||
}
|
||
}
|
||
} else {
|
||
p.RT = 0.9
|
||
}
|
||
|
||
if playTime, ok := att["playTime"].(float64); ok {
|
||
p.PlayTime = int(playTime) / 1000
|
||
}
|
||
|
||
if mid, ok := att["mid"].(string); ok {
|
||
p.Mid = mid
|
||
}
|
||
|
||
if rct, ok := att["randomCaptureTime"].(string); ok {
|
||
p.RandomCaptureTime = rct
|
||
}
|
||
|
||
if ade, ok := att["attDurationEnc"].(string); ok {
|
||
p.AttDurationEnc = ade
|
||
}
|
||
|
||
if vfce, ok := att["videoFaceCaptureEnc"].(string); ok {
|
||
p.VideoFaceCaptureEnc = vfce
|
||
}
|
||
|
||
break
|
||
}
|
||
|
||
if defaults := attachment.Defaults; defaults != nil {
|
||
if fid, ok := defaults["fid"].(string); ok {
|
||
p.FID, _ = strconv.Atoi(fid)
|
||
}
|
||
if userid, ok := defaults["userid"].(string); ok {
|
||
c.userID = userid
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// attachmentDetectVideo 从附件设置中检测并填充视频信息
|
||
func (c *XuexitongCourse) attachmentDetectVideo(attachment *entity.AttachmentSetting, p *entity.ChapterPoint) {
|
||
logger.Logger.Debug("attachmentDetectVideo 开始匹配",
|
||
zap.String("targetObjectID", p.ObjectID),
|
||
zap.Int("attachmentsCount", len(attachment.Attachments)))
|
||
|
||
found := false
|
||
for idx, att := range attachment.Attachments {
|
||
property, ok := att["property"].(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
objectid := property["objectid"]
|
||
if objectid == nil {
|
||
continue
|
||
}
|
||
objectidStr, _ := objectid.(string)
|
||
|
||
logger.Logger.Debug("检查附件",
|
||
zap.Int("index", idx),
|
||
zap.String("objectid", objectidStr),
|
||
zap.String("targetObjectID", p.ObjectID))
|
||
|
||
if objectidStr != p.ObjectID {
|
||
continue
|
||
}
|
||
|
||
found = true
|
||
logger.Logger.Debug("找到匹配的附件", zap.Int("index", idx))
|
||
|
||
if jobid, ok := property["jobid"].(string); ok {
|
||
p.JobID = jobid
|
||
} else if jobidFloat, ok := property["jobid"].(float64); ok {
|
||
p.JobID = strconv.FormatFloat(jobidFloat, 'f', -1, 64)
|
||
}
|
||
|
||
if otherInfo, ok := att["otherInfo"].(string); ok {
|
||
parts := strings.SplitN(otherInfo, "&", 2)
|
||
if len(parts) > 0 {
|
||
p.OtherInfo = parts[0]
|
||
}
|
||
logger.Logger.Debug("获取到 otherInfo", zap.String("otherInfo", p.OtherInfo))
|
||
}
|
||
|
||
if isPassed, ok := att["isPassed"].(bool); ok {
|
||
p.IsPassed = isPassed
|
||
}
|
||
|
||
if rtObj, ok := property["rt"]; ok {
|
||
switch v := rtObj.(type) {
|
||
case float64:
|
||
p.RT = v
|
||
case string:
|
||
if rt, err := strconv.ParseFloat(v, 64); err == nil {
|
||
p.RT = rt
|
||
} else {
|
||
p.RT = 0.9
|
||
}
|
||
}
|
||
} else {
|
||
p.RT = 0.9
|
||
}
|
||
|
||
if playTime, ok := att["playTime"].(float64); ok {
|
||
p.PlayTime = int(playTime) / 1000
|
||
}
|
||
if mid, ok := att["mid"].(string); ok {
|
||
p.Mid = mid
|
||
}
|
||
if rct, ok := att["randomCaptureTime"].(string); ok {
|
||
p.RandomCaptureTime = rct
|
||
}
|
||
if ade, ok := att["attDurationEnc"].(string); ok {
|
||
p.AttDurationEnc = ade
|
||
logger.Logger.Debug("获取到 attDurationEnc", zap.String("attDurationEnc", ade))
|
||
}
|
||
if vfce, ok := att["videoFaceCaptureEnc"].(string); ok {
|
||
p.VideoFaceCaptureEnc = vfce
|
||
logger.Logger.Debug("获取到 videoFaceCaptureEnc", zap.String("videoFaceCaptureEnc", vfce))
|
||
}
|
||
break
|
||
}
|
||
|
||
if !found {
|
||
logger.Logger.Warn("未找到匹配的视频附件", zap.String("objectID", p.ObjectID))
|
||
}
|
||
|
||
if defaults := attachment.Defaults; defaults != nil {
|
||
if fid, ok := defaults["fid"].(string); ok {
|
||
p.FID, _ = strconv.Atoi(fid)
|
||
}
|
||
if userid, ok := defaults["userid"].(string); ok {
|
||
c.userID = userid
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
|
||
if p.Duration <= 0 {
|
||
logger.Logger.Warn("视频时长为0,跳过", zap.String("jobID", p.JobID))
|
||
return nil
|
||
}
|
||
|
||
if p.IsPassed {
|
||
return nil
|
||
}
|
||
|
||
logger.Logger.Info("开始刷视频",
|
||
zap.String("jobID", p.JobID),
|
||
zap.Int("duration", p.Duration),
|
||
zap.Int("playTime", p.PlayTime),
|
||
zap.String("title", p.Title),
|
||
zap.String("userID", c.userID),
|
||
zap.String("cpi", p.Cpi),
|
||
zap.Int("FID", p.FID),
|
||
zap.String("otherInfo", p.OtherInfo))
|
||
|
||
playingTime := p.PlayTime
|
||
if !p.IsPassed && p.PlayTime == p.Duration {
|
||
playingTime = 0
|
||
}
|
||
|
||
selectSec := 58
|
||
isFirstSubmit := true
|
||
|
||
for {
|
||
var playReport string
|
||
var err error
|
||
|
||
isdrag := 0
|
||
if isFirstSubmit && playingTime == p.PlayTime {
|
||
isdrag = 3
|
||
}
|
||
playReport, err = c.VideoSubmitStudyTimeByPoint(ctx, p, playingTime, isdrag)
|
||
isFirstSubmit = false
|
||
|
||
if err != nil {
|
||
logger.Logger.Error("提交视频进度失败", zap.Error(err))
|
||
return err
|
||
}
|
||
|
||
var result map[string]interface{}
|
||
if err := json.Unmarshal([]byte(playReport), &result); err == nil {
|
||
if isPassed, ok := result["isPassed"].(bool); ok && isPassed {
|
||
logger.Logger.Info("视频播放完成", zap.String("jobID", p.JobID))
|
||
c.onTaskComplete()
|
||
return nil
|
||
}
|
||
}
|
||
|
||
logger.Logger.Info("视频播放", zap.Int("playingTime", playingTime), zap.Int("duration", p.Duration))
|
||
|
||
if p.Duration-playingTime < selectSec && p.Duration != playingTime {
|
||
sleepTime := p.Duration - playingTime
|
||
playingTime = p.Duration
|
||
time.Sleep(time.Duration(sleepTime) * time.Second)
|
||
} else if playingTime >= p.Duration {
|
||
time.Sleep(5 * time.Second)
|
||
} else {
|
||
playingTime += selectSec
|
||
time.Sleep(time.Duration(selectSec) * time.Second)
|
||
}
|
||
}
|
||
}
|
||
|
||
// VideoDtoFetchByPoint 获取视频详细信息(使用统一的 ChapterPoint)
|
||
func (c *XuexitongCourse) VideoDtoFetchByPoint(ctx context.Context, p *entity.ChapterPoint) error {
|
||
params := url.Values{}
|
||
params.Set("k", strconv.Itoa(p.FID))
|
||
params.Set("flag", "normal")
|
||
params.Set("_dc", strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||
|
||
var body string
|
||
err := retry.Do(func() error {
|
||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||
"User-Agent": GetUA("mobile"),
|
||
}).Get(fmt.Sprintf("%s/%s?%s", ApiVideoStatus, p.ObjectID, params.Encode()))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if resp.StatusCode() != 200 {
|
||
return errors2.NewBusinessError(resp.StatusCode(), "获取视频信息失败")
|
||
}
|
||
body = resp.String()
|
||
return nil
|
||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
var result map[string]interface{}
|
||
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
||
return err
|
||
}
|
||
|
||
if dtoken, ok := result["dtoken"].(string); ok {
|
||
p.DToken = dtoken
|
||
}
|
||
if duration, ok := result["duration"].(float64); ok {
|
||
p.Duration = int(duration)
|
||
}
|
||
if filename, ok := result["filename"].(string); ok {
|
||
if decoded, err := url.QueryUnescape(filename); err == nil {
|
||
p.Title = decoded
|
||
} else {
|
||
p.Title = filename
|
||
}
|
||
}
|
||
if status, ok := result["status"].(string); ok && status == "success" {
|
||
return nil
|
||
}
|
||
|
||
return errors2.NewBusinessError(503, "视频状态异常")
|
||
}
|
||
|
||
// VideoSubmitStudyTimeByPoint 提交视频观看进度(使用统一的 ChapterPoint)
|
||
func (c *XuexitongCourse) VideoSubmitStudyTimeByPoint(ctx context.Context, p *entity.ChapterPoint, playingTime, isdrag int) (string, error) {
|
||
clipTime := fmt.Sprintf("0_%d", p.Duration)
|
||
hash := md5.Sum([]byte(fmt.Sprintf("[%s][%s][%s][%s][%d][%s][%d][%s]",
|
||
p.ClassID, c.userID, p.JobID, p.ObjectID, playingTime*1000, VideoEncKey, p.Duration*1000, clipTime)))
|
||
enc := hex.EncodeToString(hash[:])
|
||
|
||
urlStr := "https://mooc1.chaoxing.com/mooc-ans/multimedia/log/a/" + p.Cpi + "/" + p.DToken +
|
||
"?clazzId=" + p.ClassID +
|
||
"&playingTime=" + strconv.Itoa(playingTime) +
|
||
"&duration=" + strconv.Itoa(p.Duration) +
|
||
"&clipTime=" + clipTime +
|
||
"&objectId=" + p.ObjectID +
|
||
"&otherInfo=" + p.OtherInfo +
|
||
"&courseId=" + p.CourseID +
|
||
"&jobid=" + p.JobID +
|
||
"&userid=" + c.userID +
|
||
"&isdrag=" + strconv.Itoa(isdrag) +
|
||
"&view=pc" +
|
||
"&enc=" + enc +
|
||
"&rt=" + fmt.Sprintf("%.2f", p.RT) +
|
||
"&videoFaceCaptureEnc=" + p.VideoFaceCaptureEnc +
|
||
"&dtype=Video" +
|
||
"&_t=" + strconv.FormatInt(time.Now().UnixMilli(), 10) +
|
||
"&attDuration=" + strconv.Itoa(p.Duration) +
|
||
"&attDurationEnc=" + p.AttDurationEnc
|
||
|
||
logger.Logger.Debug("VideoSubmitStudyTimeByPoint 请求参数",
|
||
zap.String("cpi", p.Cpi),
|
||
zap.String("dtoken", p.DToken),
|
||
zap.String("classID", p.ClassID),
|
||
zap.String("courseID", p.CourseID),
|
||
zap.String("objectID", p.ObjectID),
|
||
zap.String("jobID", p.JobID),
|
||
zap.String("userID", c.userID),
|
||
zap.Int("playingTime", playingTime),
|
||
zap.Int("duration", p.Duration),
|
||
zap.Int("isdrag", isdrag),
|
||
zap.String("otherInfo", p.OtherInfo),
|
||
zap.String("attDurationEnc", p.AttDurationEnc),
|
||
zap.String("videoFaceCaptureEnc", p.VideoFaceCaptureEnc),
|
||
zap.Float64("RT", p.RT),
|
||
zap.Int("FID", p.FID),
|
||
zap.String("enc", enc))
|
||
|
||
var body string
|
||
err := retry.Do(func() error {
|
||
resp, err := c.client.R().SetHeaders(map[string]string{
|
||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",
|
||
"Content-Type": "application/json",
|
||
"Accept": "*/*",
|
||
"Host": "mooc1.chaoxing.com",
|
||
"Connection": "keep-alive",
|
||
"Sec-Fetch-Site": "same-origin",
|
||
"Sec-Fetch-Mode": "cors",
|
||
"Sec-Fetch-Dest": "empty",
|
||
"Pragma": "no-cache",
|
||
"Sec-Ch-Ua-Platform": "Windows",
|
||
}).SetCookies([]*http.Cookie{
|
||
{Name: "fanyamoocs", Value: "11401F839C536D9E"},
|
||
{Name: "thirdRegist", Value: "0"},
|
||
{Name: "videojs_id", Value: "1778753"},
|
||
}).Get(urlStr)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if resp.StatusCode() != 200 {
|
||
return errors2.NewBusinessError(resp.StatusCode(), fmt.Sprintf("提交视频进度失败,状态码: %d", resp.StatusCode()))
|
||
}
|
||
body = resp.String()
|
||
return nil
|
||
}, retry.Attempts(3), retry.Delay(2*time.Second))
|
||
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return body, nil
|
||
}
|