- 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>
307 lines
8.5 KiB
Markdown
307 lines
8.5 KiB
Markdown
# 智慧树 (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
|