Greasy Fork is available in English.

东南大学抢课助手修改版

听说你抢不到课

< Feedback on 东南大学抢课助手修改版

Review: Good - script works

§
Posted: 19.12.2024

NOTE

如果没能正常进入网页(泥车的系统只要手滑退出就进不去了),那脚本将完全无法使用,有相关问题的可以把代码逻辑读懂之后自己在后端进行实现。

§
Posted: 19.12.2024
  • 如果作者有能力进行前后端协同当然是最好的,提前在前端拿信息,然后后端来发包。

当然爱发电就没必要了

julymiawAuthor
§
Posted: 19.12.2024
Edited: 19.12.2024

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。这里的code0a070340791c4cf8ac6bbfe006c422e9就是BatchID。

总结一下,如果要摆脱浏览器前端,至少需要:

  1. 通过login获取各批次的BatchID(这一步也可以手动)

  2. 通过clazz/list的API获取每一页的课程信息

  3. 通过clazz/add的API执行选课

julymiawAuthor
§
Posted: 19.12.2024

对了,后两者都需要在请求标头中包含

authorization(Token)



batchid

因此,login获取BatchID和Token是必须的,而你一开始说的“没能正常进入网页”,大概率是loginAPI未响应,那么拿不到新的token,大概率也是抢不了课的。除非你能确认token是否失效。

julymiawAuthor
§
Posted: 19.12.2024

目前我了解的其他抢课脚本的思路,除了以我的为代表的JavaScript嵌入,主流思路是模拟点击。我个人看法是,那样只会更慢。

julymiawAuthor
§
Posted: 19.12.2024

目前我的脚本最大的弱点是,串行抢课,即,一个抢课请求完成,375ms后,下一个请求才会发出。这一次服务器崩溃,和我的脚本关系不大。我的脚本本身的访问频率与手点的区别不会有那么大。

Post reply

Sign in to post a reply.