js脚本监听操作确认按钮出现并点击:

// 创建控制面板
const controlPanel = document.createElement('div');
controlPanel.id = 'buttonMonitorPanel';
controlPanel.innerHTML = `
<div style="
position: fixed;
top: 20px;
right: 20px;
width: 200px;
background: #fff;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
font-family: Arial, sans-serif;
z-index: 10000;
">
<div style="
background: #f5f5f5;
padding: 10px;
border-bottom: 1px solid #ddd;
border-radius: 8px 8px 0 0;
font-weight: bold;
">按钮监听器</div>
<div style="padding: 15px;">
<button id="startMonitor" style="
background: #4CAF50;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
">开始监听</button>
<button id="stopMonitor" style="
background: #f44336;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
">停止监听</button>
<div id="status" style="
margin-top: 10px;
font-size: 12px;
color: #666;
">状态: 未启动</div>
</div>
</div>
`;

// 添加到页面
document.body.appendChild(controlPanel);

let monitorInterval = null;
const targetButtonSelector = 'button.btn-submit[type="button"][data-bind*="continueStudy"]';

// 开始监听函数
function startMonitoring() {
  if (monitorInterval) return;
  
  document.getElementById('status').textContent = '状态: 监听中...';
  
  monitorInterval = setInterval(() => {
    const targetButton = document.querySelector(targetButtonSelector);
    if (targetButton) {
      targetButton.click();
      console.log('检测到目标按钮并已点击');
    }
  }, 1000); // 每秒检查一次
}

// 停止监听函数
function stopMonitoring() {
  if (monitorInterval) {
    clearInterval(monitorInterval);
    monitorInterval = null;
    document.getElementById('status').textContent = '状态: 已停止';
  }
}

// 绑定按钮事件
document.getElementById('startMonitor').addEventListener('click', startMonitoring);
document.getElementById('stopMonitor').addEventListener('click', stopMonitoring);

由于 ulearning 平台对学习检测有 walkingTooLong 检测机制。

如果长时间没有操作讲进行走神提示,需要点击按钮才能继续计入上课时长和并继续视频播放。

<div data-bind="if: $root.modalType() == 'suspend'">
            <div class="modal-info">
              <div class="title" data-bind="text: $root.i18nMsgText().timingLearningSuspended">
                计时学习已暂停
              </div>
              <div class="content">
                <span data-bind="text: $root.i18nMsgText().walkingTooLong">走神太久啦!计时君停止工作~</span>(<span
                  data-bind="text: $root.i18nMsgText().breakLearning">中断学习</span><span id="noStudyTime"
                  data-bind="text: noStudyTime()"></span>)
              </div>
            </div>
            <div class="modal-operation">
              <button class="btn-submit" type="button" data-bind="text: $root.i18nMsgText().continueStudy">继续学习</button>
            </div>
          </div>

做法是利用监听“继续学习”的按钮事件,并触发点击。

js逆向

课程计时:视频进度与页面停留时长的加和

Section.prototype.calculateProgress = function () {
  var section = this;
  var totalStudyTime = section.record().sectionStudyTime(); // 从历史记录获取
  
  for (var j = 0; j < section.pages().length; j++) {
    var page = section.pages()[j];
    var pageRecord = page.getRecord();
    
    totalStudyTime += pageRecord.studyTime(); // 累加当前页面的学习时间
  }
  
  return {
    totalScore: totalScore,
    totalStudyTime: totalStudyTime,  // 这里返回总学习时间
    sectionProgress: sectionProgress,
  };
};

这里的pageRecord.studyTime()记录了单个页面的学习时长,最终汇总到章节的sectionStudyTime 然后每五分钟一起转发到后端。

视频观看时间累计

在视频播放逻辑中(pageViewModel.js的timeupdateHandler),通过实时计算视频播放的时间差,更新视频观看时长(viewTime):

// pageViewModel.js #startLine: 1097-1101
var addTime = videoInstance.currentTime - element.startTime;
addTime = addTime > 0 ? addTime : 0;
element.record().viewTime(element.record().viewTime() + addTime);
element.startTime = videoInstance.currentTime;

页面停留计时

Page.prototype.startLearning = function () {
  var self = this;
  self.record().hasLearned = true;
  self.studyTimer && self.studyTimer.stopTiming();
  self.studyTimer = new Timer(self.record().studyTime()); // 创建计时器
  self.record().studyTimer = self.studyTimer;
  self.studyTimer.startTiming(function (second) {
    self.record().studyTime(second); // 实时更新学习时间
  });
};

不学习停止计时

function initNoStudyTimer() {
    var noStudyTimer;
    var noAction = new NoAction(10 * 60 * 1000, function () {
        // 10分钟无操作触发以下动作:
        
        // 1. 停止所有媒体播放
        stopAllMedia();
        self.currentPage().stopLearning();
        
        // 2. 启动中断计时器
        if (!noStudyTimer) {
            noStudyTimer = new Timer();
            noStudyTimer.startTiming(function (second) {
                // 实时更新中断时间显示
                self.noStudyTime(formatSeconds(second, true));
            });
        }
        
        // 3. 显示中断提示弹窗
        self.modalType("suspend");
        self.alertModal.show(
            function () {},
            function () {},
            function () {
                // 用户点击继续学习后的回调
                if (!isExpiredMode && !self.currentPage().isHide()) {
                    self.currentPage().startLearning();
                }
                noStudyTimer.stopTiming(); // 停止中断计时
            }
        );
    });
}

NoAction 类会监控以下事件:

document.addEventListener('mousemove', resetTimer);
document.addEventListener('keypress', resetTimer); 
document.addEventListener('click', resetTimer);
document.addEventListener('scroll', resetTimer);

鼠标移动 键盘输入 点击 和滚动 都可以算作

这里推测计时的主要逻辑是依靠视频播放进度计时,因此网站js的对时间暂停计时的方法是用stopAllMedia() 函数停止所有媒体播放。

function stopAllMedia() {
    // 停止所有视频
    $('video').each(function() {
        this.pause();
        this.currentTime = 0;
    });
    
    // 停止所有音频
    $('audio').each(function() {
        this.pause();
        this.currentTime = 0;
    });
    
    // 停止所有动画
    $('.animation-element').stop();
}

综上,挂课需要注意的是,简单的高倍速视频快速刷完视频的确会记录视频时长并以视频时长记录。

并且本身对于某一课的累计学习时长是由视频播放加上页面的停留时长共同记录的,总时长最后会再减去视频时长显示在前端的累计时长里面。因此,值得怀疑的是,后端完全有可能比较页面停留时长和视频停留时长。一旦大于,则可能被限制。 当然也可以从前端修改计时函数,转发,在此之前你需要完整分析里面关于转发后端的js,确保它不会把每次学习,开始的时间戳和离开页面结束学习的时间戳一起发送到后端,否则后端还是可能会有时间差检验机制。

并且根据有些使用倍速播放并且完成答题的学生,出现了账号被冻结的事实。

网传也有十倍速都可能不封的事实(但这远超倍速视频的允许范围内,平台在允许倍速的课程设置最高是2倍速),还是需要谨慎小心,对倍速挂课后利用本文最上方的js脚本补充对页面计时的累计。

虽然找学校处理学校都会帮你联系平台解封。

因此,良性全程挂课还是最好的手段。


收录于知乎专栏「js逆向实例」· 查看知乎原文