[Auto CQUPT Plan] 长江雨课堂自动做题(补档)
1. 前言
长江雨课堂 的 章节测试 和 课堂上的限时测验 基本上都是选择题,一题一题地艘AI太难受了,所以我抓包并逆向了一下 长江雨课堂网页版 ,本篇文章总结了其相关的API调用,方便写自动化脚本,但是因为以前抓包时 yakit 的工程文件不见了,现在的这些json结构都是我让AI根据Go结构体反推出来的,可能和实际情况有出入,不过应该不影响使用。
免责声明:
1. 本文档仅供学习研究与接口分析使用,请勿用于非法用途,否则后果自负。
2. 若你已经掌握课程知识、希望通过自动化从机械化练习中节省时间,可结合自身情况参考。
3. 平台接口、参数或风控策略可能随时变更,文档不保证长期有效。
2. 底层是怎么运作的
2.1 两个域,两套 Cookie
整个系统横跨两个独立的域,登录态互不相通:
| 域 | 负责的事 | 靠什么鉴权 |
|---|---|---|
changjiang.yuketang.cn(主站) | 扫码登录、用户 / 课程 / 作业信息、生成考试凭证 | 主站 cookie,其中有一个关键值 csrftoken |
changjiang-exam.yuketang.cn(考试系统) | 进考场、开始答卷、拉题、写答案、交卷 | 考试域独立下发的一套 cookie |
考试域的 cookie 并不是登录就自动有的,而是要在”主站生成了考试凭证 → 访问考试域 /login”这一步之后,才由服务端新下发的一批 cookie。所以“‘前面正常主站登录,后面考试接口一定可用’是一种常见的错误假设”——中间隔了这次跨域登录。
2.2 请求头约定
服务端靠请求头区分”你在调主站还是考试系统”:
| 域 | 需要的请求头 |
|---|---|
| 主站 | xtbz: ykt;其中生成考试凭证那一步额外要 X-CSRFToken |
| 考试系统 | xtbz: cloud;拉题、写答案、交卷等接口再额外要一个 x-client: web |
2.3 重定向与登录态保存
有两类接口(第 6 步登录回调、第 13 步考试系统登录)正常浏览器里表现为一个”跳转”,即服务端返回 HTTP 302。我们要判断”登录是否成功”,就得拿到”302 这次响应本身”,而不是让客户端自动跟着跳下去。所以 HTTP 客户端要关掉自动跟随重定向,改用收到 302 即视为成功:
client := &http.Client{ // 不自动跟随 302,让上层拿到重定向响应来判断登录结果 CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },}Cookie 由客户端统一保管并随后续请求自动携带;退出前写回磁盘,实现登录态跨进程复用,省得每次扫码。
2.4 接口总览
一共 20 个接口,分三组:微信扫码登录(3.1)、用户 / 课程 / 作业信息(3.2)、考试流程(3.3)。
| # | 步骤 | Method | 域 | 完整路径 | 用途 |
|---|---|---|---|---|---|
| 1 | 触发 CSRF | GET | 主站 | v/course_meta/user_info | 让服务端种下 csrftoken cookie |
| 2 | 获取 OAuth 参数 | POST | 主站 | api/v3/user/login/wechat-auth-param | 获取 appId/state |
| 3 | 取二维码页面 | GET | 微信 | open.weixin.qq.com/connect/qrconnect | 抠二维码 uid |
| 4 | 取二维码图片 | GET | 微信 | open.weixin.qq.com/connect/qrcode/{uid} | 下载二维码 PNG |
| 5 | 轮询扫码 | GET | 微信 | lp.open.weixin.qq.com/connect/l/qrconnect | 拿 wx_code |
| 6 | 换取 Session | GET | 主站 | api/v3/user/login/wechat-web-callback | 微信回调完成登录(302) |
| 7 | 用户信息 | GET | 主站 | api/v3/user/basic-info | 当前用户信息 |
| 8 | 课程列表 | GET | 主站 | v2/api/web/courses/list?identity=2 | 拿 cid(classroom_id) |
| 9 | 作业 / 章节树 | GET | 主站 | mooc-api/v1/lms/learn/course/chapter | 拿 leaf_id |
| 10 | 作业详情 | GET | 主站 | mooc-api/v1/lms/learn/leaf_info/{cid}/{leafID}/ | 单个作业详情 |
| 11 | 作业封面 | GET | 主站 | v/exam/cover | 拿 exam_id 及考试信息 |
| 12 | 生成 token | POST | 主站 | v/exam/gen_token | 换加密 token(需 CSRF) |
| 13 | 考试系统登录 | GET | 考试系统 | login | 种考试域 cookie(302) |
| 14 | 进入考场 | GET | 考试系统 | start/{examID}?isFrom=2 | 进入考场页 |
| 15 | 开始答卷 | POST | 考试系统 | exam_room/start_paper | 返回是否限时 / 剩余时间 |
| 16 | 试卷封面 | GET | 考试系统 | exam_room/cover | 考试域试卷封面 |
| 17 | 拉取题目 | GET | 考试系统 | exam_room/show_paper | 返回整卷题目 |
| 18 | 刷新剩余时间 | GET | 考试系统 | exam_room/refresh_time | 心跳保活 |
| 19 | 提交单题答案 | POST | 考试系统 | exam_room/answer_problem | 逐题写答案(暂存) |
| 20 | 交卷 | POST | 考试系统 | exam_room/submit_paper | 整卷一次性提交 |
约定:下文中
{x}表示需要由上一个接口的返回结果回填的变量;给出的 JSON 都是能直接照抄的完整结构。
3. 接口到底怎么调
3.1 微信扫码登录
登录的目的,是让主站 cookie 生效;之后的信息查询与考试流程都依赖它。
3.1.1 触发 CSRF Token(接口 1)
初始化 CSRF Token 使用的 Cookie
GET https://changjiang.yuketang.cn/v/course_meta/user_info- 请求:无入参,无特殊请求头。
- 响应:期望 HTTP 200。它没有需要解析的数据,唯一作用是触发 cookie 下发。
3.1.2 获取微信 OAuth 参数(接口 2)
要拉起微信扫码页,得先知道三件事:用哪个微信公众号(appId)、登录回调去哪个地址(redirectUri)、以及怎么防伪造(state)。
POST https://changjiang.yuketang.cn/api/v3/user/login/wechat-auth-paramContent-Type: application/json-
请求 Body:空。
-
响应(完整 JSON):
{ "code": 0, "msg": "OK", "data": { "appId": "wx4d919285d...", "state": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6IjN...", "redirectUri": "https://changjiang.yuketang.cn/api/v3/user/login/wechat-web-callback" }}| 字段(data 内) | 说明 |
|---|---|
| appId | 微信应用 ID,扫码页入参 |
| state | 状态值,扫码成功后回传校验(防 CSRF) |
| redirectUri | 回调地址,扫码页入参 |
code = 0 表示成功。
3.1.3 获取二维码页面(接口 3)
用上一步的参数去打开微信的扫码页。微信不是返回一张图片给我们,而是返回一段 HTML,真正的二维码地址藏在里面(形如 /connect/qrcode/一长串字符)。
GET https://open.weixin.qq.com/connect/qrconnect?appid={appId}&scope=snsapi_login&redirect_uri={redirectUri}&state={state}&login_type=jssdk&self_redirect=true&styletype=&sizetype=&bgcolor=&rst=&ts=&stylelite=1- 请求:
appid/redirect_uri/state来自 3.1.2,其余为固定值。 - 响应:HTML 文本(string),没有 JSON。
<!-- 普通登录 --> <div class="js_normal_login web_qrcode_img_area"> <div class="web_qrcode_img_wrp"> <img class="js_qrcode_img web_qrcode_img" src="/connect/qrcode/031TVnLl0m7zGa1w"/> <button style="display:none" type="button" class="weui-btn_reset web_qrcode_refresh_btn js_refresh_qrcode" title="刷新"><i class="web_qrcode_refresh_icon"></i></button> <i style="display:none" class="weui-loading js_refresh_qrcode_loading web_qrcode_refresh_loading"></i> <div style="display:none" class="web_qrcode_img_mask js_refresh_qrcode_mask"></div> </div> <div class="web_qrcode_switch_wrp js_switchToFast_wrp" style="display:none;"> <button type="button" class="weui-btn_reset weui-link js_switchToFast web_qrcode_switch">快捷登录</button> </div> </div>需要提取其中
/connect/qrcode/031TVnLl0m7zGa1w部分的031TVnLl0m7zGa1w
关键实现:要从 HTML 里抠出二维码地址里的 uid,用一条正则即可:
// 从扫码页 HTML 中提取二维码 uidre := regexp.MustCompile("/connect/qrcode/([A-Za-z0-9]+)")res := re.FindStringSubmatch(content)uid := res[1] // 后续下载二维码、轮询扫码都要用到3.1.4 下载二维码图片(接口 4)
拿到 uid 后,把二维码 PNG 下载下来,渲染给用户扫码。
GET https://open.weixin.qq.com/connect/qrcode/{uid}- 请求:路径参数
uid来自 3.1.3。 - 响应:
Content-Type以image/开头的 PNG 二进制字节。
关键实现 1:下载前先校验响应头确实是图片,避免拿到一段错误页当图片处理:
contentType := resp.Header.Get("Content-Type")if contentType[:5] != "image" { return fmt.Errorf("Content-Type Error: %s", contentType)}关键实现 2:终端里没有图形界面时,可以把 PNG 转成”文字二维码”。原理是取每个像素的灰度,把上下两行合成一个字符,用 ' '、'▄'、'▀'、'█' 四个半块字符表达”上黑 / 下黑 / 都黑”:
// 把二维码缩放后在外围加一圈白边框,便于在深色终端上识别blocks := []rune{' ', '▄', '▀', '█'}for y := 0; y < total; y += 2 { // 每两行像素输出一行 for x := 0; x < total; x++ { idx := 0 if isBlack(img.At(x, y)) { idx |= 2 } // 上黑 if y+1 < total && isBlack(img.At(x, y+1)) { idx |= 1 } // 下黑 fmt.Printf("%c", blocks[idx]) } fmt.Println()}3.1.5 轮询扫码状态(接口 5)
二维码显示出来后,要反复询问微信”用户扫了没有”。每 5 秒问一次,直到拿到扫码授权码。
GET https://lp.open.weixin.qq.com/connect/l/qrconnect?uuid={uid}- 请求:Query 参数
uuid来自 3.1.3。 - 响应:一段文本(非 JSON)。用户若已扫码,会包含
window.wx_code='xxx';还没扫则没有。
window.wx_errcode=405;window.wx_code='031GcKFa1pT...';提取其中的
031GcKFa1pT...这段值
关键实现:先用正则判断是否出现 wx_code,取到就跳出循环:
func extractWxCode(content string) (string, bool) { matches := regexp.MustCompile(`window\.wx_code='([^']+)'`).FindStringSubmatch(content) if len(matches) == 0 { return "", false // 尚未扫码,继续等 } return matches[1], true}外层再套一个轮询循环:
var wxCode stringfor { time.Sleep(5 * time.Second) // 每 5 秒轮询一次
content, _ := httpGet(pollURL) // 请求本接口 if code, ok := extractWxCode(content); ok { wxCode = code // 拿到授权码,登录成功 break }}3.1.6 换取 Session(接口 6)
拿到 wx_code 后,带着它和 state 去访问回调地址,主站据此完成登录、下发登录态 cookie。
GET https://changjiang.yuketang.cn/api/v3/user/login/wechat-web-callback?path=/authorize/wx-qrlogin%3Fsuccess%3D1&code={wx_code}&state={state}- 请求:
code来自 3.1.5,state来自 3.1.2;path是固定值并做了 URL 编码。 - 响应:期望 HTTP 302(见 2.3,收到 302 即判定登录成功)。
到这里主站登录完成,返回的 Cookie 里会包含 sessionid=1x7v7rguev0z... ,使用这个 Cookie ,§3 的接口 7 以后都能用了。
3.2 用户 / 课程 / 作业信息(主站)
在调考试接口之前,得先把三个 ID 找齐:cid(课程 id)、leaf_id(作业 id)、exam_id(考试 id)。这条链路一环扣一环。
3.2.1 用户基本信息(接口 7)
登录后第一次请求,确认登录态真的生效,同时拿用户信息。
GET https://changjiang.yuketang.cn/api/v3/user/basic-info- 请求:无入参、无特殊请求头。
- 响应(完整 JSON):
{ "code": 0, "msg": "OK", "data": { "id": "114514", "avatar": "https://thirdwx.qlogo.cn/mmopen/vi_32/...", "name": "达啥必", "school": "重庆邮电大学", "schoolNumber": "2029111000", "edited": true, "binding": true, "role": 2, "dateJoined": "2029-13-32T25:61:61", "nickname": "微信用户" }}3.2.2 课程列表(接口 8)
列出当前用户所有课程,我们要找的是课程的 classroom_id,下文统称 cid。
GET https://changjiang.yuketang.cn/v2/api/web/courses/list?identity=2- 请求:固定 Query 参数
identity=2。 - 响应(完整 JSON):
{ "errcode": 0, "errmsg": "Success", "data": { "list": [ { "term": 202502, "name": "2026春-思想道德与法治-20班", "short_name": null, "students_count": 89, "time": 1772605262000, "teacher": { "user_id": 0, "name": "教师", "avatar": "" }, "published_kg_id": null, "create_time": 1772011715000, "role": 6, "agent_id": null, "project_type": null, "ways_of_operation": null, "university_course_series_id": "202502-xxxxxxxx", "color_code": 3, "classroom_id": 0, "top_time": 0, "course": { "en_name": null, "manage_permission": false, "name": "思想道德与法治", "short_name": null, "admin_id": 0, "platform": null, "university_id": 0, "type": 2, "id": 2123735, "is_pro": true, "university_mini_logo": "https://qn-next.xuetangx.com/xxxx.png", "university_name": "大学" } } ] }}重点:classroom_id(即 cid)是后续所有章节 / 考试查询的入参,务必取出记录。
3.2.3 章节树(接口 9)
给一门课的 cid,返回它的章节结构;作业 / 考试就是某一章节下的”叶子节点”。
GET https://changjiang.yuketang.cn/mooc-api/v1/lms/learn/course/chapter?cid={cid}&classroom_id={cid}Header: xtbz: ykt- 请求:Query 参数
cid和classroom_id都填 3.2.2 拿到的值;Headerxtbz: ykt。 - 响应(完整 JSON):
{ "success": true, "data": { "course_id": 123, "course_name": "高等数学 A", "course_chapter": [ { "section_leaf_list": [ { "name": "第一次小测", "is_locked": false, "start_time": 1710000000, "chapter_id": 1, "section_id": null, "leaf_type": 5, "id": 1945538, "is_show": true, "end_time": 1713600000, "score_deadline": 1713600000, "is_open_type": false, "is_score": true, "is_assessed": true, "order": 1, "leafinfo_id": 1945538 } ], "order": 1, "id": 1, "name": "第一章 极限" } ] }}重点:叶子节点的 leafinfo_id(或 id)就是我们要的 leaf_id;leaf_type 标记类型(如 5 = 作业 / 考试)。
3.2.4 作业详情(接口 10)
拿单个作业的详细信息,比如发布人、截止时间、计分比例。不是考试流程的必需步骤,但能帮你判断”这个作业做没做、值不值得做”。
GET https://changjiang.yuketang.cn/mooc-api/v1/lms/learn/leaf_info/{cid}/{leafID}/Header: xtbz: ykt, classroom-id: {cid}- 请求:路径参数
cid、leafID(来自 3.2.3);Headerxtbz: ykt与classroom-id。 - 响应(完整 JSON):
{ "success": true, "msg": "", "data": { "upgrade_sku_status": null, "publish_time": 1710000000.0, "is_assessed": true, "locked_reason": null, "price": 0, "sku_name": "", "user_role": 2, "be_in_force": true, "current_price": 0, "third_platform_code": "", "class_start_time": 1710000000, "upgrade_sku_id": null, "university_id": 1, "score_deadline": 1713600000.0, "course_id": 123, "user_id": 57510820, "teacher": {}, "is_score": true, "third_platform_name": "", "sku_id": 0, "is_deleted": false, "name": "第一次小测", "content_info": { "status": "", "video_user_play": null, "expand_discuss": false, "score_evaluation": { "score_proportion": { "proportion": 0.1 }, "score": 100, "id": 1, "name": "平时成绩" }, "download": [], "is_score": true, "is_discuss": false, "remark": { "remark": "" }, "cover_desc": "", "cover_thumbnail": "", "media": {}, "cover": "", "leaf_type_id": 5, "is_attachment_download": false, "context": "" }, "classroom_id": "24211265", "is_locked": false, "leaf_type": 5, "id": 1945538, "has_classend": false, "classroom_short_name": null, "is_open_type": false, "class_end_time": 0 }}3.2.5 作业封面(接口 11)
打开考试的入口页数据:标题、总分、题目数、截止时间、要不要人脸认证等。这里的入参 exam_id 才是考试域真正认的考试 ID。
GET https://changjiang.yuketang.cn/v/exam/cover?exam_id={examID}&classroom_id={cid}Header: xtbz: ykt- 请求:Query 参数
exam_id、classroom_id(cid);Headerxtbz: ykt。 - 响应(完整 JSON):
{ "status": 0, "msg": "", "success": true, "data": { "server_time": 1710000000, "app_front_random_snap": 0, "paper_count": 1, "max_retry_random": 0, "exam_exercise_video_attachment_1G": 0, "identity_auth": 1, "deadline": 1713600000, "university_id": 1, "capture_screen": 0, "max_screen_cuts_num": 0, "max_retry": 0, "is_sku_time": false, "organize_problem_method": 0, "classroom_name": null, "course_name": null, "title": "第一次小测", "OCR_type": "", "is_single_paper": true, "organize_paper_method": 0, "show_score_time": 0, "en_copy": false, "is_offline": false, "open_screen_cuts": 0, "access_restriction_info_for_graph": "", "start_time": 1710000000, "open_access_restriction_for_graph": false, "online_proctor": 0, "show_score": true, "description": "", "show_answer": false, "total_score": 100, "en_crypt": false, "user_role": 2, "user": { "user_name": "张三", "user_id": 57510820, "avatar": "", "language": null, "school_number": "2022..." }, "access_restriction_info": "", "app_capture_screen": 0, "encrypt": "", "limit_early_submission_time": 0, "show_perm": true, "limit_early_submission": false, "page_switch_detection": 0, "is_manual_review": 0, "is_partial_participation": false, "classroom_id": 24211265, "face_auth_status": { "online_proctor": 0, "has_idphoto": false }, "problem_dict_count": 0, "has_problem_dict": false, "problem_count": 10, "platform_type": 1, "client_type": "", "limit": 7200, "open_access_restriction": false, "way_of_score": 0, "web_random_take_face_photo": 0, "force_confirm": false, "result": { "status": 0, "score": null, "incorrect_count": null, "unfinished_count": null } } }ID 获取链小结:课程列表 →
classroom_id(cid)→ 章节树 → 叶子节点leafinfo_id(leaf_id)→ 封面 →exam_id。三个 ID 齐了,才能进考试域。
3.3 考试流程(考试系统 changjiang-exam.yuketang.cn)
拿到 exam_id 和 cid 后,流程从主站的”生成 token”起步,然后切到考试域。
3.3.1 生成考试 Token(接口 12,主站)
主站签发一个加密凭证 token,交给考试系统去验证”这个用户确实有资格考这场试”。这是全程唯一需要 CSRF 的接口。
POST https://changjiang.yuketang.cn/v/exam/gen_tokenHeader: xtbz: ykt, X-CSRFToken: {主站 cookie 中的 csrftoken}Content-Type: application/json- 请求 Body:
{ "exam_id": "1945538", "classroom_id": "24211265" }| 字段 | 类型 | 说明 |
|---|---|---|
| exam_id | string | 考试 ID |
| classroom_id | string | 课程 / 班级 ID(即 cid) |
关键实现:请求头里的 X-CSRFToken 不是随便记的,它来自主站 cookie 里那个 csrftoken。实现时要从 cookie 容器中把它取出来回填:
for _, cookie := range jar.Cookies(mainDomainURL) { if cookie.Name == "csrftoken" { req.Header.Set("X-CSRFToken", cookie.Value) break }}req.Header.Set("xtbz", "ykt")- 响应(完整 JSON):
{ "status": 0, "msg": "", "success": true, "data": { "token": "H1tf/67TNyDZ...", "exam_host": "changjiang-exam.yuketang.cn", "user_id": 57510820 } }| 字段(data 内) | 说明 |
|---|---|
| token | 加密凭证,下一步 /login 的 crypt 入参 |
| exam_host | 考试系统域名(据此切域) |
| user_id | 用户 ID,下一步 /login 入参 |
3.3.2 考试系统登录(接口 13,跨域)
把上一步的 token 作为 crypt 交给考试系统 /login,服务端校验通过后,在考试域种下专属 cookie。此后考试域的接口都靠这套 cookie 鉴权。
GET https://changjiang-exam.yuketang.cn/login?exam_id={examID}&user_id={user_id}&crypt={token}&next=https://changjiang-exam.yuketang.cn/start/{examID}?isFrom=2&language=zh| Query 参数 | 说明 |
|---|---|
| exam_id | 考试 ID |
| user_id | 来自 3.3.1 的 data.user_id |
| crypt | 来自 3.3.1 的 data.token |
| next | 登录后跳转地址(进入考场页) |
| language | zh |
关键实现:next 是拼出来的,指向”进场”路径:
nextUrl := fmt.Sprintf("https://changjiang-exam.yuketang.cn/start/%d?isFrom=2", examID)- 响应:期望 HTTP 302;服务端在考试域下发 cookie,跨域登录完成。这一步失败,后面考试域所有请求都白搭。
3.3.3 进入考场(接口 14)
模拟”点进考场”,让服务端确认考生就位。
GET https://changjiang-exam.yuketang.cn/start/{examID}?isFrom=2- 请求:路径参数
examID,固定 QueryisFrom=2。 - 响应:期望 HTTP 200。
3.3.4 开始答卷(接口 15)
正式开始答卷。返回是否限时,以及已用 / 剩余时间——记住剩余时间,后面答题节奏要照着它走。
POST https://changjiang-exam.yuketang.cn/exam_room/start_paperHeader: xtbz: cloudContent-Type: application/json- 请求 Body:
{ "exam_id": "1945538" }- 响应(完整 JSON):
{ "errcode": 0, "errmsg": "", "data": { "has_limit": true, "time_past": 0, "time_left": 7200 } }| 字段(data 内) | 说明 |
|---|---|
| has_limit | 是否限时 |
| time_past | 已用时间(秒) |
| time_left | 剩余时间(秒) |
3.3.5 试卷封面(接口 16,考试系统)
考试域内的试卷封面,和主站的 exam/cover 数据同源。一般是想在考试域内部拿”总分、题数、截止时间”时用。
GET https://changjiang-exam.yuketang.cn/exam_room/cover?exam_id={examID}Header: xtbz: cloud, x-client: web- 请求:Query 参数
exam_id。 - 响应(完整 JSON,
data结构与 3.2.5 主站封面同源):
{ "errcode": 0, "errmsg": "", "data": { "title": "第一次小测", "description": "", "has_problem_dict": false, "problem_dict_count": 0, "course_name": null, "classroom_name": null, "total_score": 100, "problem_count": 10, "start_time": 1710000000, "deadline": 1713600000, "is_sku_time": false, "limit": 7200, "show_perm": true, "show_answer": false, "show_score": true, "show_score_time": 0, "online_proctor": 0, "classroom_id": 24211265, "university_id": 1, "platform_type": 1, "organize_problem_method": 0, "organize_paper_method": 0, "client_type": "", "en_copy": false, "en_crypt": false, "app_capture_screen": 0, "capture_screen": 0, "app_front_random_snap": 0, "is_single_paper": true, "paper_count": 1, "open_screen_cuts": 0, "max_screen_cuts_num": 0, "is_offline": false, "open_access_restriction": false, "access_restriction_info": "", "open_access_restriction_for_graph": false, "access_restriction_info_for_graph": "", "OCR_type": "", "encrypt": "", "is_manual_review": 0, "identity_auth": 1, "web_random_take_face_photo": 0, "page_switch_detection": 0, "exam_exercise_video_attachment_1G": 0, "is_partial_participation": false, "limit_early_submission": false, "limit_early_submission_time": 0, "force_confirm": false, "max_retry": 0, "max_retry_random": 0, "way_of_score": 0, "user": { "user_id": 57510820, "user_name": "张三", "school_number": "2022...", "avatar": "", "language": null }, "cut_count": 0, "result": { "status": 0, "score": null, "incorrect_count": null, "unfinished_count": null }, "face_auth_status": { "online_proctor": 0, "has_idphoto": false }, "server_time": 1710000000, "user_role": 2 } }3.3.6 拉取试卷题目(接口 17,核心接口)
整条链路的重中之重:把整张卷子拉下来。返回一个题目数组,每道题包含题干、题型、选项、分值。拿到它,后面的”自动作答”才有得下手。
GET https://changjiang-exam.yuketang.cn/exam_room/show_paper?exam_id={examID}Header: xtbz: cloud, x-client: web- 请求:Query 参数
exam_id。 - 响应(完整 JSON,
problems[]为完整字段集):
{ "errcode": 0, "errmsg": "", "data": { "problems": [ { "Body": "题干,可能含 <img>HTML 图片标签", "Type": "single_choice", "TypeText": "单选题", "data": {}, "Score": 5.0, "score": 5.0, "index": 1, "Options": [ { "key": "A", "value": "选项A" }, { "key": "B", "value": "选项B" }, { "key": "C", "value": "选项C" }, { "key": "D", "value": "选项D" } ], "Version": "", "HasRemark": false, "LibraryID": 0, "max_retry": 0, "TemplateID": 0, "difficulty": 0, "library_id": 0, "problem_id": 92134642, "ProblemType": 1, "template_id": 0, "problem_type": 1, "isContinueWithWrong": 0, "create_template_type": "" } ], "has_problem_dict": false, "font": "", "title": "第一次小测" }}| 字段(problem 内) | 说明 |
|---|---|
| problem_id | 题目标识,提交时原样回传 |
| Body | 题干,可能带 <img> 图片标签 |
| Type / TypeText | 题型标识 / 中文名 |
| Options[].key/value | 选项 key(A/B/C…)与内容 |
| index / Score | 题序 / 分值 |
题干里可能是图片形式的题(
<img>标签)。可用正则(?i)<img[^>]*src=...把图片 URL 抠出来,再交给多模态模型识别,或转成 Base64 后喂给模型——这就是”自动做题里的读题”环节。
3.3.7 刷新剩余时间(接口 18,心跳保活)
答题过程中反复刷新倒计时;同时也作为一种”心跳”,让服务端认为页面还活着。
GET https://changjiang-exam.yuketang.cn/exam_room/refresh_time?exam_id={examID}Header: xtbz: cloud, x-client: web- 请求:Query 参数
exam_id。 - 响应(完整 JSON):
{ "errcode": 0, "errmsg": "", "data": { "has_limit": true, "time_past": 120, "time_left": 7080 } }3.3.8 提交单题答案(接口 19,暂存)
把某道题的答案”写”上去。注意:这只是暂存,不等同于交卷。一道一道地调,避免一次性把答案全塞进去。
POST https://changjiang-exam.yuketang.cn/exam_room/answer_problemHeader: xtbz: cloud, x-client: web, Content-Type: application/json- 请求 Body:
{ "results": [ { "problem_id": 92134642, "result": ["A"], "time": 1710000000000 } ], "exam_id": 1945538, "record": []}| 字段 | 说明 |
|---|---|
| results[].problem_id | 来自 3.3.6 的 problem_id |
| results[].result | 答案数组:单选 ["A"],多选 ["A","C"],填空 / 主观题填答案串 |
| results[].time | 作答时间戳(毫秒) |
| exam_id | 考试 ID(数字型) |
| record | 空数组,占位 |
- 响应(完整 JSON):
{ "errcode": 0, "errmsg": "", "data": {} }errcode = 0 表示成功。
3.3.9 交卷(接口 20,整卷一次性提交)
全部答案填完后,把整卷一次性正式提交。一旦提交就不可逆,务必确认答案都填完再调。
POST https://changjiang-exam.yuketang.cn/exam_room/submit_paperHeader: xtbz: cloud, x-client: web, Content-Type: application/json- 请求 Body(注意为 snake_case,
exam_id是字符串):
{ "exam_id": "1945538", "results": [ { "problem_id": 92134642, "result": ["A"], "time": 1710000000000, "show_answer": "", "is_answered": true, "is_save": true } ]}| 字段 | 说明 |
|---|---|
| results[].show_answer | 留空字符串 |
| results[].is_answered | true |
| results[].is_save | true |
对比 3.3.8:
answer_problem逐题暂存,submit_paper整卷提交。后者多了show_answer / is_answered / is_save三个字段,且exam_id用字符串。
- 响应(完整 JSON):
{ "errcode": 0, "errmsg": "", "data": {} }4. 一次考试的完整链路
把上面 20 个接口串起来,一次自动考试分四个阶段。
4.1 阶段 A:首次登录(微信扫码)
只有在主站 cookie 没有或失效时才需要。按 1→6:
GET /v/course_meta/user_info→ 种下csrftokencookie。POST /api/v3/user/login/wechat-auth-param(Body 空)→ 得到appId、state、redirectUri。GET open.weixin.qq.com/connect/qrconnect?…→ 拿 HTML,正则/connect/qrcode/([A-Za-z0-9]+)抠出uid。GET open.weixin.qq.com/connect/qrcode/{uid}→ 拿 PNG(校验Content-Type: image/*),渲染成二维码。- 每 5s 轮询
GET lp.open.weixin.qq.com/connect/l/qrconnect?uuid={uid}→ 正则window\.wx_code='([^']+)'提取wx_code,取到跳出。 GET /api/v3/user/login/wechat-web-callback?code={wx_code}&state={state}→ 302,主站登录完成。
4.2 阶段 B:准备考试参数
GET /api/v3/user/basic-info→ 确认登录态。GET /v2/api/web/courses/list?identity=2→ 取classroom_id(cid)。GET /mooc-api/v1/lms/learn/course/chapter?cid={cid}(xtbz: ykt)→ 取leaf_id。GET /v/exam/cover?exam_id={examID}&classroom_id={cid}(xtbz: ykt)→ 确认exam_id。
4.3 阶段 C:进入考试并拉题
POST /v/exam/gen_token(带X-CSRFToken,Body{"exam_id","classroom_id"})→ 取token、user_id、exam_host。GET 考试系统 /login?crypt={token}&user_id={user_id}&next=…→ 302,考试域 cookie 种下。GET 考试系统 /start/{examID}?isFrom=2→ 进考场。POST 考试系统 /exam_room/start_paper(Body{"exam_id"})→ 开始答卷,得time_left。GET 考试系统 /exam_room/show_paper?exam_id=→ 得到整卷problems[]。
4.4 阶段 D:自动作答并提交
- (可选)
GET /exam_room/refresh_time→ 心跳保活。 - 对每题:
POST /exam_room/answer_problem(Body{"results":[{"problem_id","result","time"}],"exam_id","record":[]})→ 暂存答案。 - 全部完成:
POST /exam_room/submit_paper(整卷 snake_case,含show_answer/is_answered/is_save)→ 交卷。
4.5 时序图
顺序约束:第 12 步”考试系统登录”必须成功,其后考试域接口才可用;
answer_problem先于submit_paper,且submit_paper只能调一次。
5. 相关项目
如果你想直接看可运行的版本,下面两个仓库已把这套链路做了落地,可作为对照实现:
目标平台基础地址:
https://changjiang.yuketang.cn # 主站(登录 / 鉴权 / 信息查询)https://changjiang-exam.yuketang.cn # 考试系统(考场 / 题目 / 提交)
6. 结尾
没有了喵,谢谢阅读

文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!