Greasy Fork is available in English.
听说你抢不到课
当然爱发电就没必要了
NOTE
如果没能正常进入网页(泥车的系统只要手滑退出就进不去了),那脚本将完全无法使用,有相关问题的可以把代码逻辑读懂之后自己在后端进行实现。
可以具体一点吗?没太看懂你的意思...
我大概整理一下抢课逻辑:
核心逻辑是:
request({
url: "/elective/clazz/add",
method: "POST",
headers: {
batchId: enrollDict[key].courseBatch,
"content-type": "application/x-www-form-urlencoded",
},
data: Qs.stringify({
clazzType: enrollDict[key].courseType,
clazzId: enrollDict[key].courseCode,
secretVal: enrollDict[key].secretVal,
}),
})
这一部分参考了官方代码中的:
selectCourse: function(a) {
var c = this;
var b = "";
c.dialogParam.canSelectParam.selectedjxbTmp = a;
if ((c.sysParam.needBook == "1" && c.lcParam.currentBatch.canSelectBook == "1") || c.lcParam.currentBatch.typeCode == "01" || a.hasTest == "1") {
c.selectVolunteerdata(a)
} else {
c.$msgbox({
title: "提醒",
message: "确认选择课程吗?",
type: "warning",
showCancelButton: true,
closeOnClickModal: false,
callback: function(d) {
if (d == "confirm") {
var e = {
clazzType: c.teachingClassType,
clazzId: c.dialogParam.canSelectParam.selectedjxbTmp.JXBID,
secretVal: c.dialogParam.canSelectParam.selectedjxbTmp.secretVal
};
c.addCourse(e)
}
}
})
}
},
和
addCourse: function(a) {
var b = this;
axios.post("/elective/clazz/add", a).then(function(c) {
if (c.data.code == 200 && b.showAddMsg) {
b.$message({
type: "success",
message: "已进入选课队列,请稍后",
duration: window.messageTime,
showClose: true
});
b.dialogParam.canSelectParam.showCanSelected = false
} else {
if (c.data.code == 301) {
b.$msgbox({
title: "提醒",
message: c.data.msg + ",确认选择课程吗?",
type: "warning",
showCancelButton: true,
closeOnClickModal: false,
callback: function(d) {
if (d == "confirm") {
a.isConfirm = 1;
axios.post("/elective/clazz/add", a).then(function(e) {
if (e.data.code == 200 && b.showAddMsg) {
b.$message({
type: "success",
message: "已进入选课队列,请稍后",
duration: window.messageTime,
showClose: true
});
b.dialogParam.canSelectParam.showCanSelected = false
}
})
}
}
})
} else {
b.dialogParam.canSelectParam.showCanSelected = false
}
}
})
},
其中,最关键的就是secretVal,每次token变化,所有的secretVal就失效了,需要重新获取。
在grablessons.min.js中,开放了名为grablessonsVue
的api,目前是基于这一api来根据clazzId获取完整信息的。
选课系统采用js动态渲染,所有渲染逻辑都在这个js文件中,理论上,只用读懂它的代码逻辑,就可以独立开发一个无网页的后端了。
目前我已经使用的api有:
弹窗tip方法:
//提示
let tip = grablessonsVue.$message;
tip({
type: "error", // warning, success
message: "请稍候,正在终止上一个抢课进程",
duration: 1000,
});
选课批次代码:
grablessonsVue.lcParam.currentBatch.code
这个值就是上面的batchId,同一批次是固定的。
然后,为了使用CourseID(code)获取CourseType和SecretVal,我们需要:
let currentType = grablessonsVue.teachingClassType
let currentCourseList = grablessonsVue.courseList;
let courseCode = code.substring(0, 8);
let teacherCode = code.substring(8);
let courseFlag = false,
teacherFlag = false;
for (let course of currentCourseList) {
// 检查课程是否存在
if (course.KCH === courseCode) {
courseFlag = true;
// 检查教师是否存在
if (grablessonsVue.teachingClassType !== "XGKC") {
for (let teacher of course.tcList) {
if (teacher.KXH === teacherCode) {
enrollDict[code] = {
courseBatch: grablessonsVue.lcParam.currentBatch.code,
courseCode: teacher.JXBID,
courseType: currentType,
courseName: course.KCM,
teacherName: teacher.SKJS,
secretVal: teacher.secretVal,
};
teacherFlag = true;
}
}
} else {
if (course.KXH === teacherCode) {
enrollDict[code] = {
courseBatch: grablessonsVue.lcParam.currentBatch.code,
courseCode: course.JXBID,
courseType: currentType,
courseName: course.KCM,
teacherName: course.SKJS,
secretVal: course.secretVal,
};
teacherFlag = true;
}
}
}
}
但是,以上api有一个限制,就是只能获取当前页存在的课程。
如果想自由获取任意课程,注意到浏览器向https://newxk.urp.seu.edu.cn/xsxk/elective/clazz/list
发送了POST请求,携带了以下信息:
{
"teachingClassType": "TJKC", // 推荐课程
"pageNumber": 1, // 当前页码
"pageSize": 10, // 每页课程
"orderBy": "", // 排序标准
"campus": "1" // 未知
}
在获得的返回值中,包含了我们需要的所有信息,甚至,与你的专业无关,因为它获取的是该课程的全校的信息。
{
"KCH": "B0493010", // 课程号
"KCM": "通信原理(跨学科选课)", // 课程名
"BJS": 1, // 班级数
"KCLB": "专业主干课", // 课程列表
"hours": "32", // 课时数
"XF": "2", // 学分
"SFYX": "0", // 是否已选
"KKDW": "信息科学与工程学院", // 开课单位
"KCXZ": "任选", // 课程性质
"tcList": [ // 教学班列表(全校)
{
"teachCampus": "***",
"SFXZXB": "0",
"isRetakeClass": "3",
"department": "100202",
"teachingMethod": "讲授",
"TJBJ": "090221,090222,090223,09J221,581221,581222,581223,61I221,61J221",
"NSKRL": 0,
"NVSKRL": 0,
"NSXKRS": 41,
"NVSXKRS": 4,
"limitKindList": [
{
"wid": "250814C41BCF4767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "090221",
"limitDesc": "090221",
"childLimitKind": []
},
{
"wid": "250814C41BD14767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "090222",
"limitDesc": "090222",
"childLimitKind": []
},
{
"wid": "250814C41BD34767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "090223",
"limitDesc": "090223",
"childLimitKind": []
},
{
"wid": "250814C41BD54767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "09J221",
"limitDesc": "09J221",
"childLimitKind": []
},
{
"wid": "250814C41BD74767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "581221",
"limitDesc": "581221",
"childLimitKind": []
},
{
"wid": "250814C41BD94767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "581222",
"limitDesc": "581222",
"childLimitKind": []
},
{
"wid": "250814C41BDB4767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "581223",
"limitDesc": "581223",
"childLimitKind": []
},
{
"wid": "250814C41BDD4767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "61I221",
"limitDesc": "61I221",
"childLimitKind": []
},
{
"wid": "250814C41BDF4767E0638346400AE1AB",
"teachingClassID": "202420253B049301001",
"code": "01",
"name": "班级",
"limitType": "1",
"limitValue": "61J221",
"limitDesc": "61J221",
"childLimitKind": []
}
],
"SKSJ": [ // 授课时间
{
"teachingClassID": "202420253B049301001",
"KCH": "B0493010",
"KCM": "通信原理(跨学科选课)",
"SKZC": "1111111111111111",
"SKZCMC": "1-16周",
"SKXQ": "1",
"KSJC": "3",
"JSJC": "4",
"KSSJ": "09:50",
"JSSJ": "10:35",
"timeType": "1",
"SKDD": "教四-402",
"KXH": "01",
"SKJS": "姜明",
"campus": "1",
"XQ": "九龙湖"
}
],
"schoolTerm": "2024-2025-3", // 学期
"JXBID": "202420253B049301001", // 教学班ID,包含了学期信息,课程编号,教学班编号
"campus": "1",
"XQ": "九龙湖", // 校区
"KCH": "B0493010", // 课程号
"KCM": "通信原理(跨学科选课)", // 课程名
"KXH": "01", // 教学班编号
"SKJS": "姜明", // 授课教师
"SKJSLB": "姜明(副研究员)|101010895|",
"KKDW": "信息科学与工程学院", // 开课单位
"teachingPlace": "1-16周 星期一 3-4节 教四-402",
"teachingPlaceHide": "1-16周 星期一 第3-4节 ",
"XS": "32",
"XF": "2",
"examType": "2",
"hasTest": "0",
"isTest": "0",
"hasBook": "0",
"numberOfSelected": 45,
"numberOfFirstVolunteer": 0,
"classCapacity": 45, // 课容量
"pyKrl": 0,
"kxKrl": 0,
"fxKrl": 0,
"cxKrl": 0,
"yjsKrl": 0,
"pyYxrs": 0,
"kxYxrs": 0,
"fxYxrs": 0,
"cxYxrs": 0,
"yjsYxrs": 0,
"schoolClassMapStr": "61J221,581223,090221,61I221,581222,090222,581221,09J221,090223",
"KRL": 45,
"SKJSZC": "姜明(副研究员)",
"YXRS": 45,
"DYZYRS": 0,
"SFYX": "0",
"SFYM": "1",
"secretVal": // 校验码
"bzEMQUvuXb+PyQEjfwK6zW8wXZncwlDRqDY/Kc+WG2ISp5qzl4XTsvh/b5a5DFlO6GC5qe5CmBqNFticKqlAsNrmS5rT9L8x4rynz0X+G/piC0Oho0IadjVogCeJqjKOLzAagm+jmxwK7SD5ZPRtXmZOp6M3GCB+jJZWG7Ta4qA=",
"KCXZ": "任选",
"KCLB": "专业主干课",
"SFCT": "1",
"SFXZXK": "",
"XGXKLB": "",
"DGJC": "0",
"SFKT": "1",
"conflictDesc": "[领导力素养(校企)][04]上课时间[1-16周 星期一 第3-5节]-[通信原理(跨学科选课)][01]上课时间[1-16周 星期一 第3-4节]冲突",
"testTeachingClassID": "",
"YPSJDD": "1-16周 星期一 3-4节 教四-402",
"ZYDJ": ""
}
],
"courseUrl": null,
"ZFX": null,
"CXCKLX": null,
"KCLY": null
}
所以理论上,不使用学校开放的api,也可以获取选课必备的全部信息。
对了,batchID是在login时获取的,登录成功会返回:
[
{
"code": "a8c4a5f209194ac7ac49c40dfcd1c60c",
"name": "2024-2025学年春季学期辅修课程预选",
"noSelectReason": null,
"noSelectCode": null,
"canSelect": "1",
"schoolTerm": "2024-2025-3",
"beginTime": "2024-12-19 13:00:00",
"endTime": "2024-12-25 16:00:00",
"tacticCode": "01",
"tacticName": "可选可退",
"typeCode": "02",
"typeName": "正选",
"needConfirm": null,
"confirmInfo": null,
"isConfirmed": "0",
"schoolTermName": "2024-2025学年春季学期",
"weekRange": "1-16周",
"canSelectBook": "0",
"canDeleteBook": "0",
"multiCampus": "1",
"multiTeachCampus": "0",
"menuList": null,
"noCheckTimeConflict": "0"
},
{
"code": "0a070340791c4cf8ac6bbfe006c422e9",
"name": "2024-2025学年春季学期预选课",
"noSelectReason": null,
"noSelectCode": null,
"canSelect": "1",
"schoolTerm": "2024-2025-3",
"beginTime": "2024-12-19 13:00:00",
"endTime": "2024-12-25 16:00:00",
"tacticCode": "01",
"tacticName": "可选可退",
"typeCode": "02",
"typeName": "正选",
"needConfirm": null,
"confirmInfo": null,
"isConfirmed": "0",
"schoolTermName": "2024-2025学年春季学期",
"weekRange": "1-16周",
"canSelectBook": "0",
"canDeleteBook": "0",
"multiCampus": "0",
"multiTeachCampus": "0",
"menuList": null,
"noCheckTimeConflict": "0"
},
{
"code": "eb4fe80a9cdc46cd8faba31cde95fa48",
"name": "2024-2025学年春季学期重修选课",
"noSelectReason": null,
"noSelectCode": null,
"canSelect": "1",
"schoolTerm": "2024-2025-3",
"beginTime": "2024-12-19 13:00:00",
"endTime": "2024-12-25 16:00:00",
"tacticCode": "01",
"tacticName": "可选可退",
"typeCode": "02",
"typeName": "正选",
"needConfirm": null,
"confirmInfo": null,
"isConfirmed": "0",
"schoolTermName": "2024-2025学年春季学期",
"weekRange": "1-16周",
"canSelectBook": "0",
"canDeleteBook": "0",
"multiCampus": "1",
"multiTeachCampus": "0",
"menuList": null,
"noCheckTimeConflict": "1"
}
]
和token。这里的code
如0a070340791c4cf8ac6bbfe006c422e9
就是BatchID。
总结一下,如果要摆脱浏览器前端,至少需要:
通过login获取各批次的BatchID(这一步也可以手动)
通过clazz/list的API获取每一页的课程信息
通过clazz/add的API执行选课
对了,后两者都需要在请求标头中包含
authorization(Token)
和
batchid
因此,login获取BatchID和Token是必须的,而你一开始说的“没能正常进入网页”,大概率是loginAPI未响应,那么拿不到新的token,大概率也是抢不了课的。除非你能确认token是否失效。
目前我了解的其他抢课脚本的思路,除了以我的为代表的JavaScript嵌入,主流思路是模拟点击。我个人看法是,那样只会更慢。
目前我的脚本最大的弱点是,串行抢课,即,一个抢课请求完成,375ms后,下一个请求才会发出。这一次服务器崩溃,和我的脚本关系不大。我的脚本本身的访问频率与手点的区别不会有那么大。
NOTE
如果没能正常进入网页(泥车的系统只要手滑退出就进不去了),那脚本将完全无法使用,有相关问题的可以把代码逻辑读懂之后自己在后端进行实现。