[Auto CQUPT Plan] HIT SEE 做题自动化(补档)
1. 背景
本人苦傻福 C语言 在线编程作业久矣,一个学期TM要刷300道题目,绝大部分题目毫无营养可言并且浪费时间,简直是厨圣!所以为了解决这个困境,顺便学习一下爬虫,就拿 Yakit 对 HIT SEE 这个平台做了一下抓包和逆向,实现了 LLM自动做题、键盘敲击记录伪造、计划任务批量做题。
免责声明:
1. 本程序仅供学习使用,若你用这个玩意然后挂科了,那只能说明你是’小可爱‘,与作者一律无关!
2. 若你已经掌握C语言并且觉得C语言的OJ作业限制了你对新技术、新事务的学习,那你可以食用本项目。
2. 接口概览
| # | 作用 | Method | 路径 | 关键入参 | 返回关键字段 |
|---|---|---|---|---|---|
| 1 | 登录 | POST | /student/auth | code,pwd | sid |
| 2 | 题目列表 | GET | /student/list-tag | qBaseType,level,sid | [{id,content}] |
| 3 | 建会话 | POST | /student/choose-q | [{qBaseType,number,level,tagId}] | sessionId |
| 4 | 取题详情 | GET | /student/record/{sessionId} | sid | content,globalId,qType |
| 5 | 提交答案 | POST | /student/post-answer | answerPaper,verificationCode | code,timeLeft |
| 6 | 确认交卷 | GET | /student/submit | sessionId,sid | 字面量 1 |
| 7 | 获取分数 | GET | /student/score-result/{sessionId} | sid | [{score,scoreResult}] |
所有路径前缀为
https://cprg.cqupt.edu.cn/train/api。
3. 接口文档
3.1 用户鉴权(登录)
获取 sid,后续所有操作的前提。
请求
POST https://cprg.cqupt.edu.cn/train/api/student/auth?sid=请求 Body(JSON):
{ "code": "", "pwd": ""}| 字段 | 类型 | 说明 |
|---|---|---|
| code | str | 学号 |
| pwd | str | 密码 |
响应
{ "code": "", "expire": 0, "id": 0, "ip": "", "name": "", "sid": ""}| 字段 | 类型 | 说明 |
|---|---|---|
| code | string | 学号 |
| expire | int64 | 过期时间(Unix 时间戳) |
| id | int64 | 用户 ID |
| ip | string | 登录 IP |
| name | string | 姓名 |
| sid | string | 身份识别码,其余所有接口的鉴权凭据(需自行在会话内保存) |
3.2 获取题目列表
查询可用的 题目标签(tag)列表,拿到 tagId 供出题使用。
请求
GET https://cprg.cqupt.edu.cn/train/api/student/list-tag?qBaseType=&level=&sid=Query 参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| qBaseType | int64 | 问题类型:0 单选 1 多选 2 判断 3 填空 4 编程 5? |
| level | int64 | 难度:0 随机 1 初学者 2 容易 3 中等 4 困难 5 极难 |
| sid | string | 身份识别码 |
响应(数组)
[ { "id": 87, "content": "U1-字符处理", "contentEn": "U1-Character Processing" }, { "id": 3, "content": "U1-数据类型、运算符与表达式", "contentEn": "U1-Data types, operators and expressions" }]| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 标签 ID(tagId) |
| content | string | 中文题目 |
| contentEn | string | 英文题目 |
3.3 创建答题会话
按 标签 + 题目数量 向服务端要一个答题会话,得到 sessionId。sessionId 是整个做题流程的核心句柄。
请求
POST https://cprg.cqupt.edu.cn/train/api/student/choose-q?sid=Body 传的是数组(源码固定只放 1 个元素):
[ { "qBaseType": 4, "number": 1, "level": 0, "tagId": 87 }]| 字段 | 类型 | 说明 |
|---|---|---|
| qBaseType | int64 | 问题类型 |
| number | int64 | 问题数量(1 道) |
| level | int64 | 问题难度 |
| tagId | int64 | 标签 ID(来自 2) |
响应
{ "code": 0, "sessionId": "3e03d593195b4e81bedb0dd4b54d7177"}| 字段 | 类型 | 说明 |
|---|---|---|
| code | int64 | 状态码(0 表示成功) |
| sessionId | string | 答题会话 ID(核心句柄) |
3.4 获取题目详情
拿到题目内容 content 与题目的全局 ID globalId、题型 qType。
请求
GET https://cprg.cqupt.edu.cn/train/api/student/record/{sessionId}?sid=| 位置 | 字段 | 说明 |
|---|---|---|
| PATH | sessionId | 会话 ID |
| Query | sid | 身份识别码 |
响应(关键字段)
{ "code": 0, "startTime": "2025-11-08T00:49:31.768254367", "state": 0, "timeLeft": 4525, "totalTime": 4525, "paper": { "questions": [ { "id": 1350, "content": "大小写字母转换问题:\n ...", "point": 10.0, "qType": 5, "language": 1, "globalId": "3e74118614fd49adbb575ca4cec8d1b3", "options": [] } ] }, "answerPaper": { "q0": [], "q1": [], "q2": [], "q3": [], "q4": [ { "answer": "#include <stdio.h>\n\nint main()\n{\n\treturn 0;\n}", "keySequence": [], "globalId": "3e74118614fd49adbb575ca4cec8d1b3", "qType": 5 } ], "q5": [] }, "examName": "", "studentCode": "", "studentName": "", "buyCode": false, "score": 0.0, "timeline": [ { "createTime": "...", "actionCode": 0, "actionData": "" } ]}实际做题只用其中三个字段:
| 字段 | 说明 |
|---|---|
| content | 题目题干(丢给 LLM 生成答案) |
| globalId | 题目的全局 ID(提交时原样回传,用于定位题目) |
| qType | 题型(提交时原样回传,q4 / 编程题里为 5) |
结构说明:
paper.questions[0]是题目内容;answerPaper.q4[0]是编程题对应的作答槽位。单选(q0)/ 多选(q1)/ 判断(q2)/ 填空(q3)/ 编程(q4)/ q5 一一对应题型的作答槽位。
3.5 提交答案
这里可以用于伪造键盘敲击记录
把答案写回服务器。注意:这只是”暂存/写答案”,不等于最终交卷;且必须附带动态生成的 verificationCode。
请求
POST https://cprg.cqupt.edu.cn/train/api/student/post-answer?sid=Body(JSON):
{ "sessionId": "f3caabbb4070471f9123da021b8f50d2", "answerPaper": { "q0": [], "q1": [], "q2": [], "q3": [], "q4": [ { "answer": "#include <stdio.h>\n\nint main()\n{\n\tprintf(\"%c\", c + 32);\n\treturn 0;\n}", "keySequence": [], "globalId": "f227f40921514aa994931b6c82e47361", "qType": 5 } ], "q5": [] }, "verificationCode": "551e50eb37c5d80ea40230d3bbe2cc71"}| 字段 | 类型 | 说明 |
|---|---|---|
| sessionId | string | 会话 ID |
| answerPaper.q4[].answer | string | 答案代码 |
| answerPaper.q4[].keySequence | []int64 | 键盘敲击记录(伪造时不传则为空数组) |
| answerPaper.q4[].globalId | string | 来自 4 的 globalId |
| answerPaper.q4[].qType | int64 | 来自 4 的 qType |
| verificationCode | string | 动态验证码,见 §3.8,校验失败可能被拒 |
响应
{ "code": 0, "timeLeft": 3816 }| 字段 | 类型 | 说明 |
|---|---|---|
| code | int64 | 状态码(0 表示成功) |
| timeLeft | int64 | 剩余时间(秒) |
3.6 确认交卷
写入答案后调用,正式提交该会话。
GET https://cprg.cqupt.edu.cn/train/api/student/submit?sessionId=&sid=| 位置 | 字段 | 说明 |
|---|---|---|
| Query | sessionId | 会话 ID |
| Query | sid | 身份识别码 |
成功响应:字面量 1(字符串)。
3.7 获取分数
请求
GET https://cprg.cqupt.edu.cn/train/api/student/score-result/{sessionId}?sid=| 位置 | 字段 | 说明 |
|---|---|---|
| PATH | sessionId | 会话 ID |
| Query | sid | 身份识别码 |
响应(数组)
[ { "globalId": "f227f40921514aa994931b6c82e47361", "score": 0.0, "qType": 5, "scoreResult": { "state": -1, "score": 0.0, "similarity": 0.0, "keyPointScore": 0.0, "staticMatchScore": 0.0, "dynamicTestScore": 0.0, "failedTestCase": { "output": "Press a key and then press Enter:\nz", "testCase": { "input": "Z\r\n", "output": "Press a key and then press Enter:z" } }, "compilerOutput": "", "compilerErrorOutput": "", "directOutputCheat": false } }]| 字段(scoreResult 内) | 说明 |
|---|---|
| state | 评测状态(-1 通常表示未通过 / 失败) |
| score | 得分 |
| similarity / keyPointScore / staticMatchScore / dynamicTestScore | 各维度评分(相似度、要点、静态匹配、动态测试) |
| failedTestCase | 失败用例,含输入 / 期望输出 / 实际输出 |
| compilerOutput / compilerErrorOutput | 编译输出 / 编译错误输出 |
| directOutputCheat | 是否判定为「直接输出作弊」 |
成功返回 JSON 数组;失败(无记录)返回空。
3.8 验证码(verificationCode)生成算法
verificationCode 不是随机值,而是由 当前日期时间(精确到分钟)+ sessionId 拼接后做 MD5 得到,服务端据此校验提交是否”新鲜、未过期”。
formattedTime = "YYYY-MM-DD HH:MM" + sessionIdverificationCode = MD5(formattedTime)Go 实现(ojSupport.go):
func (root *OjSupport) GenerateVerificationCode(sessionID string) string { now := time.Now()
formattedTime := fmt.Sprintf("%d-%02d-%02d %02d:%02d %s", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), sessionID, )
hash := md5.Sum([]byte(formattedTime)) return fmt.Sprintf("%x", hash)}即拼接串形如:
2025-11-08 00:49 f3caabbb4070471f9123da021b8f50d2注意:年份是四位(
%d),月份 / 日期 / 时 / 分是补零两位(%02d),sessionId以空格分隔直接拼在末尾。
3.9 键盘敲击记录(keySequence)伪造
平台的反作弊会记录答题时的键盘事件,存入 keySequence。这段时间被当作”真正在敲代码”的行为特征。
keySequence由成对的元素组成:[时间戳, 键码, 时间戳, 键码, ...]- 时间戳为毫秒级的
time.Now().UnixNano() / 1e6 - 键码在固定键值池里随机挑:
{1,2,3,4,5,6,7,8,8,45,46,47,66,54,67,0,44,26,33,29}
Go 实现(FakeKeyPress()):
keyList := []int64{1,2,3,4,5,6,7,8,8,45,46,47,66,54,67,0,44,26,33,29}
for i := 0; i < 3; i++ { localList := []int64{ time.Now().UnixNano() / 1e6, // 时间戳(ms) keyList[rand.Intn(len(keyList))], // 随机键码 } keyPressList = append(keyPressList, localList...) time.Sleep(time.Duration(root.weightedRandom()) * time.Millisecond) // 控制敲击间隔}为了让敲击间隔更像真人,用加权随机分布:70% 概率间隔 0–600ms,30% 概率间隔 600–2000ms:
func (root *OjSupport) weightedRandom() int { rand.Seed(time.Now().UnixNano()) if rand.Float64() < 0.7 { return rand.Intn(601) // 0-600ms } return 600 + rand.Intn(1401) // 600-2000ms}GenTimeSpan() 会在”做题耗时”窗口内,每隔 0–2s 调用一次 FakeKeyPress(),模拟持续敲击。
4. 完整做题流程时序
- 登录拿
sid(§3.1) - 选标签(可选)拿
tagId(§3.2) - 建会话拿
sessionId(§3.3) - 取题拿
content/globalId/qType(§3.4) - (可选)做题窗口内伪造
keySequence(§3.9) - 回填答案 +
verificationCode(§3.5、§3.8) - 确认交卷(§3.6)
- 查分(§3.7)
5. 相关项目
本文档基于对 OJByeBye 项目源码(
core/utils/ojSupport/ojSupport.go、core/models/models.go、core/runc/runc.go)及现有分析文档(README.md、docs/C语言做题系统逆向分析.md)的整理归纳而成。目标平台基础地址:
https://cprg.cqupt.edu.cn/train所有接口均位于
https://cprg.cqupt.edu.cn/train/api/student/*下,核心鉴权凭据为登录后返回的sid(身份识别码),几乎所有请求都要携带它(GET 走 Query,POST 走 Query 或 PATH)。
6. 结尾
没有了喵,谢谢阅读

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