Files
course/zhihuishu_api_final.md
nanxunandClaude Fable 5 cd486a36ef feat: UOOC/Zhihuishu dual-platform brushing platform
- ASP.NET Core 9 Web API backend with JWT auth, EF Core MySQL
- Vue 3 + Vite + Pinia + Ant Design Vue frontend
- Multi-platform connection management (UOOC & Zhihuishu)
- Video brushing with AES-CBC encryption for Zhihuishu
- Multi-task queue with cross-platform parallel execution
- Task persistence via MySQL database
- Progress tracking with inline catalog enrichment
- Mobile-responsive UI

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-24 15:31:32 +08:00

8.5 KiB
Raw Permalink Blame History

智慧树 (zhihuishu.com) API 逆向分析

日期: 2026-06-23 | 全部接口已验证通过


一、概述

智慧树有两套 API 体系:

体系 域名 加密 课程类型
Zhidao studyservice-api / onlineservice-api AES-CBC (secretStr) 共享学分课
Hike hikeservice / studyresources / hike-teaching 无加密 校内学分课

二、登录流程

4.1 密码登录(需要滑块验证码)

Step 1: GET passport.zhihuishu.com/login → 获取 Session Cookie (JSESSIONID等)

Step 2: POST passport.zhihuishu.com/user/validateAccountAndPassword
  Body: secretStr=Base64(encodeURIComponent(JSON.stringify({
    account: "手机号",
    password: "密码",
    validate: "滑块验证码token"
  })))
  → {status: 1, uuid: "...", pwd: "一次性密码"}

Step 3: POST appcomm-user.zhihuishu.com/.../checkNeedAuth
  Body: uuid=xxx
  → {rt: {needAuth: 0}}

Step 4: GET passport.zhihuishu.com/login?pwd={pwd}&service={service}
  → 302重定向链 → 设置 CASLOGC Cookie
  注意: 必须用 requests 库跟随重定向,urllib 不会自动跨域传 Cookie

加密方式: btoa(encodeURIComponent(JSON)) — 就是 Base64 + URL编码
(不是 AES!这是登录专用加密)

4.2 滑块验证码

网易易盾,captchaId: 75f9f716460a422f89a628f50fd8cc2b

<script src="https://cstaticdun.126.net/load.min.js"></script>
<script>
initNECaptcha({
  element: '#captcha',
  captchaId: '75f9f716460a422f89a628f50fd8cc2b',
  mode: 'popup',
  onVerify: function(err, data) {
    // data.validate 就是需要的 token
  }
}, onload, onerror);
</script>

三、Zhidao API(共享学分课)

5.1 AES 加密

KEY_MAP = {
    "home":  b"7q9oko0vqb3la20r",   # 课程列表
    "video": b"azp53h0kft7qi78q",   # 视频/学习/进度
    "qa":    b"kcGOlISPkYKRksSK",   # 弹题
}
IV = b"1g3qqdh4jvbskb9x"

def aes_encrypt(data_str, key=KEY_MAP["video"]):
    pad_len = 16 - len(data_str) % 16
    padded = data_str + chr(pad_len) * pad_len
    cipher = AES.new(key, AES.MODE_CBC, IV)
    return base64.b64encode(cipher.encrypt(padded.encode())).decode()

请求格式:

POST /api/endpoint
Content-Type: application/x-www-form-urlencoded
Body: secretStr={AES密文}&dateFormate={毫秒时间戳}

⚠️ dateFormate 必须同时在加密 JSON 内表单字段外,且值相同。

5.2 课程列表

POST onlineservice-api.zhihuishu.com/gateway/t/v1/student/course/share/queryShareCourseInfo
加密密钥: HOME_KEY
加密前: {"status":0,"pageNo":1,"pageSize":10,"dateFormate":时间戳}

返回:
{
  "code": 0,
  "result": {
    "totalCount": 1,
    "courseOpenDtos": [{
      "secret": "RAC_id",     // 后续 API 的课程标识
      "courseName": "课程名",
      "recruitId": 389213,    // 招生ID
      "courseId": 1000076607, // 课程ID
      "schoolName": "学校",
      "teacherName": "教师"
    }]
  }
}

5.3 章节/视频列表

先调 gologin:
  GET studyservice-api.zhihuishu.com/login/gologin?fromurl=...

再调:
  POST studyservice-api.zhihuishu.com/gateway/t/v1/learning/videolist
  加密密钥: VIDEO_KEY
  加密前: {"recruitAndCourseId":"RAC_id","dateFormate":时间戳}

返回:
{
  "code": 0,
  "data": {
    "courseId": 1000076607,
    "videoChapterDtos": [{
      "id": 1001076678,       // chapterId
      "name": "章节名",
      "videoLessons": [{
        "id": 1001297571,     // lessonId
        "name": "小节名",
        "videoSmallLessons": [{
          "id": 4001,         // smallLessonId (单视频时=0)
          "videoId": 63921147,// 视频ID
          "videoSec": 600,    // 总时长(秒)
          "chapterId": 1001076678
        }]
      }]
    }]
  }
}

5.4 视频弹题信息

POST studyservice-api.zhihuishu.com/gateway/t/v1/popupAnswer/loadVideoPointerInfo
加密密钥: VIDEO_KEY
加密前: {"lessonId":...,"lessonVideoId":...,"recruitId":...,"courseId":...,"dateFormate":时间戳}

返回: questionPoint 数组 (弹题时间点 + 题目ID)

5.5 视频播放 URL

GET newbase.zhihuishu.com/video/initVideo?jsonpCallBack=result&videoID={videoId}
返回: JSONP,包含 lines[0].lineUrl

5.6 学习状态

POST studyservice-api.zhihuishu.com/gateway/t/v1/learning/queryStuyInfo
加密密钥: VIDEO_KEY
加密前:
{
  "lessonIds": [1001297571],    // 课时ID列表
  "lessonVideoIds": [],         // 子视频ID列表,单视频课时(smallLessonId=0)填空数组[]
  "recruitId": 389213,          // 招生ID
  "dateFormate": 1782200000000
}
⚠️ lessonVideoIds 为 0 时不要传 [0],传空数组 []

返回:
{
  "code": 0,
  "data": {
    "lv": {
      "4001": { "watchState": 1, "studyTotalTime": 600 }
    },
    "lesson": {
      "1001297571": { "watchState": 1, "studyTotalTime": 600 }
    }
  }
}
watchState: 0=未看完, 1=已看完
studyTotalTime: 已学习秒数

5.7 提交学习进度

Step 1 (prelearningNote):
  POST studyservice-api.zhihuishu.com/gateway/t/v1/learning/prelearningNote
  加密密钥: VIDEO_KEY
  加密前: {"ccCourseId":...,"chapterId":...,"isApply":1,"lessonId":...,
           "lessonVideoId":...,"recruitId":...,"videoId":...,"dateFormate":时间戳}
  返回: data.studiedLessonDto.id → Base64编码得到 learningTokenId

Step 2 (saveDatabaseIntervalTimeV2):
  POST studyservice-api.zhihuishu.com/gateway/t/v1/learning/saveDatabaseIntervalTimeV2
  加密密钥: VIDEO_KEY
  加密前: {
    "ewssw": "0,1,2",          // watchPoint
    "sdsew": getEv([...]),     // EV混淆的参数
    "zwsds": learningTokenId,  // Base64编码的token
    "courseId": ...,
    "dateFormate": 时间戳
  }

5.8 EV 混淆算法

def getEv(data_list, key="zzpttjd"):
    """XOR 混淆"""
    data = ';'.join(map(str, data_list))
    key_cycle = (ord(c) for _ in iter(int,1) for c in key)
    ev = ''
    for c in data:
        tmp = hex(ord(c) ^ next(key_cycle)).replace('0x', '')
        if len(tmp) < 2: tmp = '0' + tmp
        ev += tmp[-4:]
    return ev

# saveDatabaseIntervalTimeV2 的 raw_ev 参数:
raw_ev = [
    recruitId, lessonId, smallLessonId, videoId, chapterId,
    '0',                                    # studyStatus
    played_time - last_submit,              # 本次播放时长
    played_time,                            # 累计播放时长  
    HMS(played_time),                       # HH:MM:SS 格式
    uuid + "zhs"                            # UUID后缀
]

四、Hike API(校内学分课)

无需 AES 加密,直接 GET 请求,带 Cookie 即可。

6.1 课程列表

GET hikeservice.zhihuishu.com/student/course/aided/getMyCourseList?uuid={uuid}&data={UTC时间}
返回: result.startInngcourseList (注意拼写)

6.2 章节/资源树

GET studyresources.zhihuishu.com/studyResources/stuResouce/queryResourceMenuTree?courseId={id}
返回: rt 数组,childList 非空=目录,childList=null=文件,dataType=3=视频

6.3 视频信息

GET studyresources.zhihuishu.com/studyResources/stuResouce/stuViewFile?courseId={id}&fileId={id}
返回: dataId (视频流ID), totalTime, studyTime

6.4 提交学习记录

GET hike-teaching.zhihuishu.com/stuStudy/saveStuStudyRecord?uuid=...&courseId=...&fileId=...
    &studyTotalTime=...&startWatchTime=...&endWatchTime=...&startDate=...&endDate=...
    &signature=MD5(SALT + uuid + courseId + fileId + studyTotalTime + startDate + endDate + endWatchTime + startWatchTime + uuid)

SALT = "o6xpt3b#Qy$Z"

五、Cookie 体系

登录后获取的关键 Cookie

Cookie 域名 说明
CASLOGC passport.zhihuishu.com URL编码JSON,含uuid/realName/userId
JSESSIONID passport.zhihuishu.com Session ID
SERVERID 各子域名 服务器路由
SESSION onlineservice-api / studyservice-api API会话 (通过CAS重定向获取)

⚠️ 登录时必须跟随完整 CAS 重定向链,否则缺少 onlineservice-api 的 SESSION Cookie 导致 API 返回 401。


六、关键注意事项

  1. AES JSON 格式: json.dumps(data, separators=(',', ':')) — 紧凑格式,无空格
  2. dateFormate 双写: 加密 JSON 内和表单字段都要有,且值相同
  3. lessonVideoIds 不能含 0: 单视频课时 smallLessonId=0,此时传空数组 []
  4. CAS 重定向: 必须用 requests.Session() 自动跟随,urllib 不会跨域传 Cookie
  5. latin-1 编码: Cookie 值保持 URL 编码状态(纯 ASCII),不要解码成中文
  6. Encrypt 已变更: 登录接口用 btoa(encodeURIComponent(JSON)),不是 AES