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

307 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 智慧树 (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`
```html
<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 加密
```python
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 混淆算法
```python
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