比較提交

...

17 次程式碼提交

共有 14 個檔案被更改,包括 1430 行新增81 行删除

5
.gitignore vendored
查看文件

@ -1,4 +1,7 @@
.idea/*
/config.yaml
node_modules/*
/pnpm-lock.yaml
/pnpm-lock.yaml
/arr.json
backup
resetCookieRequest.txt

查看文件

@ -4,10 +4,10 @@
#### 特性:
- 监控
- 抢课
- 监控(检视某几门课的剩余人数变化)
- 抢课(当某几门课剩余人数大于0时自动抢课)
- 换课(在课表冲突的情况下先退课再抢课,如果抢课失败进行回滚)
- 将操作信息发送至QQ(调用Qmsg酱API)
- 将监控、抢课、换课操作信息发送至QQ(调用Qmsg酱API)
#### 环境

查看文件

@ -1,7 +1,15 @@
# 教务cookie
cookie: "JSESSIONID=asd;"
# 在 https://qmsg.zendee.cn 申请
qmsgKey: "v9631222364dd7ae99909cb63b7445c5"
# 学号
username: "2020123456"
# 密码
password: "abcdefg123123"
# 在 https://qmsg.zendee.cn 获取
qmsgKey: "15rg6s5t1h6t1s5g66"
## telegram api 域名
#tgApiDomain: "api.telegram.org"
## @BotFather 建立机器人获取
#tgApiKey: ""
## @getidsbot 获取
#tgChatId: -454888515
# 轮询间隔毫秒
interval: 100
# 监控列表

查看文件

@ -5,17 +5,22 @@
"main": "index.js",
"scripts": {
"test": "ts-node src/test.ts",
"start": "ts-node src/index.ts"
"start": "ts-node src/index.ts",
"draw": "ts-node src/draw.ts",
"resetCookie": "ts-node src/resetCookieRequest.ts"
},
"author": "",
"license": "ISC",
"dependencies": {
"@types/node": "^16.9.1",
"@types/qs": "^6.9.7",
"@types/tough-cookie": "^4.0.1",
"axios": "^0.21.4",
"axios-cookiejar-support": "^1.0.1",
"axios-retry": "^3.1.9",
"dayjs": "^1.10.7",
"qs": "^6.10.1",
"tough-cookie": "^4.0.0",
"ts-node": "^10.2.1",
"typescript": "^4.4.3",
"yaml": "^1.10.2"

查看文件

@ -57,7 +57,7 @@ export async function replaceProcess(courseList: ICourse[], exit: ICourse) {
}
if (parseInt(single.syrs) > 0) {
logAndNotifyUser('[replace]发现 ' + single.kcmc + ' 剩余人数为' + single.syrs + ',进行换课 at ' + getTimeNow())
if (!(await exitCourse(exitJsonCourse, exit.channel))) {
if (!(await exitCourse(exitJsonCourse))) {
logAndNotifyUser('[replace]退课 ' + exitJsonCourse.kcmc + ' 失败 at ' + getTimeNow())
}
if (await acquireCourse(single, course.channel)) {

1029
src/des.js Normal file

檔案差異因為檔案過大而無法顯示 載入差異

46
src/draw.ts Normal file
查看文件

@ -0,0 +1,46 @@
import {countStr, getTimeNow, logAndNotifyUser, resetCookie, sduAxios, sleep} from "./includes"
import * as fs from "fs"
export async function draw() {
while (true) {
let html = ''
try {
let resp = await sduAxios.get("jsxsd/xsxkjg/comeXkjglb?isktx=true")
html = JSON.stringify(resp.data)
} catch (e) {
console.log(e)
}
if (countStr(html, 'cqOper') < 2) {
logAndNotifyUser('登录失败,重设cookie ' + getTimeNow())
try {
await resetCookie()
} catch (e) {
console.log(e)
}
continue
}
if (countStr(html, 'cqOper') > 2) {
logAndNotifyUser('可以抽签,正在进行抽签')
fs.writeFileSync('draw-' + Date.now() + '.html', html)
let arr = [
'2021202222605',// 英语
'2021202225984',// 周易
'2021202225817',// 刻瓷
'2021202222614',// 众智
'2021202222613',// 移动互联网
]
for (let id of arr) {
try {
let resp = await sduAxios.get('/jsxsd/xsxkjg/xscqOper?jx0404id=' + id + '&cqqk=3')// 3抽的中,2抽不中
logAndNotifyUser(JSON.stringify(resp.data))
} catch (e) {
console.log(e)
}
}
}
console.log('轮询中:' + getTimeNow())
await sleep(1000)
}
}
draw()

60
src/health.ts Normal file
查看文件

@ -0,0 +1,60 @@
import {IJsonCourse} from "./types"
import {JsonCourseList} from "./poll"
import {findJsonCourse, getTimeNow} from "./includes"
import {appConfig} from "./config"
import {scalarOptions} from "yaml"
import Str = scalarOptions.Str
let lastJsonCourseList: { bx: IJsonCourse[], xx: IJsonCourse[], rx: IJsonCourse[] }
let firstRunHealth = true
export async function health() {
let totalAcquireCount = 0
let totalExitCount = 0
let acquireNames: string[] = []
let exitNames: string[] = []
if (firstRunHealth) {
console.log('[health]程序health监控启动完毕')
firstRunHealth = false
} else {
for (let channel of appConfig.channels) {
for (let lastCourse of lastJsonCourseList[channel]) {
let currentCourse = findJsonCourse({
kch: lastCourse.kch,
kxh: parseInt(lastCourse.kxh),
channel: channel
})
if (currentCourse == undefined) {
continue
}
if (parseInt(currentCourse.syrs) > parseInt(lastCourse.syrs)) {
totalExitCount += parseInt(currentCourse.syrs) - parseInt(lastCourse.syrs)
if (!exitNames.includes(currentCourse.kcmc)) {
exitNames.push(currentCourse.kcmc)
}
} else {
let num = parseInt(lastCourse.syrs) - parseInt(currentCourse.syrs)
totalAcquireCount += num
if (!acquireNames.includes(currentCourse.kcmc) && num > 0) {
acquireNames.push(currentCourse.kcmc)
}
}
}
}
console.log('[health]实时总退课人次:' + totalExitCount + '(' + shuffle(exitNames).slice(0, 3).join(',') + '等),总选课人次:'
+ totalAcquireCount + '(' + shuffle(acquireNames).slice(0, 3).join(',') + '等) at ' + getTimeNow())
}
lastJsonCourseList = JSON.parse(JSON.stringify(JsonCourseList))
setTimeout(health, 60 * 1000)
}
function shuffle(array: any[]) {
let currentIndex = array.length, randomIndex
while (currentIndex != 0) {
randomIndex = Math.floor(Math.random() * currentIndex)
currentIndex--;
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]]
}
return array
}

查看文件

@ -3,19 +3,31 @@ import {appConfig} from "./config"
import * as qs from "qs"
import dayjs from "dayjs"
import {ICourse, IJsonCourse, TChannel} from "./types"
import {JsonCourseList} from "./poll"
import {JsonCourseList, reqBody} from "./poll"
import axiosRetry from "axios-retry"
import tough from "tough-cookie"
import axiosCookieJarSupport from "axios-cookiejar-support"
import {strEnc} from "./des"
import {scalarOptions} from "yaml"
import * as process from "process"
let sduAxios = axios.create({
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Cookie': appConfig.cookie
'X-Requested-With': 'XMLHttpRequest',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0',
},
baseURL: 'https://bkzhjx.wh.sdu.edu.cn/',
timeout: 5,
// maxRedirects: 0
timeout: 5000,
transformResponse: data => {
try {
return JSON.parse(data)
} catch (_e) {
return data
}
},
// maxRedirects: 10
})
axiosRetry(sduAxios, {
retries: 2,
shouldResetTimeout: true,
@ -24,13 +36,21 @@ axiosRetry(sduAxios, {
export async function logAndNotifyUser(text: string) {
console.log(text)
await axios.post('https://qmsg.zendee.cn/send/' + appConfig.qmsgKey, qs.stringify({
msg: text
}), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
// await axios.post('https://qmsg.zendee.cn/send/' + appConfig.qmsgKey, qs.stringify({
// msg: text
// }), {
// headers: {
// 'Content-Type': 'application/x-www-form-urlencoded'
// }
// })
try {
await axios.post('https://' + appConfig.tgApiDomain + '/bot' + appConfig.tgApiKey + '/sendMessage', qs.stringify({
chat_id: appConfig.tgChatId,
text: text
}))
} catch (e) {
console.log(e)
}
}
export function sleep(ms: number) {
@ -46,21 +66,105 @@ export function findJsonCourse(course: ICourse): IJsonCourse | undefined {
}
export async function acquireCourse(course: IJsonCourse, channel: TChannel): Promise<boolean> {
if (channel == 'bx' || channel == 'xx') {
} else {
let segment = ''
switch (channel) {
case 'bx':
segment = 'bxxkOper'
break
case 'xx':
segment = 'xxxkOper'
break
case 'rx':
segment = 'ggxxkxkOper'
break
}
try {
let resp = await sduAxios.get('/jsxsd/xsxkkc/' + segment + '?kcid=' + course.jx02id + '&cfbs=null&jx0404id=' + course.jx0404id + '&xkzy=&trjf=')
if (resp.data.success === true) {
return true
} else {
logAndNotifyUser(resp.data.message)
if (resp.data.message.includes('当前账号已在别处登录')) {
await resetCookie()// TODO 加锁、优化
}
return false
}
} catch (e) {
return false
}
return true
}
export async function exitCourse(course: IJsonCourse, channel: TChannel): Promise<boolean> {
if (channel == 'bx' || channel == 'xx') {
} else {
export async function exitCourse(course: IJsonCourse): Promise<boolean> {
try {
let resp = await sduAxios.get('/jsxsd/xsxkjg/xstkOper?jx0404id=' + course.jx0404id)
if (resp.data.success === true) {
return true
} else {
logAndNotifyUser('[exitCourse]退课失败:' + resp.data.message)
if (resp.data.message.includes('当前账号已在别处登录')) {
await resetCookie()
}
return false
}
} catch (e) {
logAndNotifyUser('[exitCourse]网络异常,退课失败')
return false
}
return true
}
let resetCookieLock = false
export async function resetCookie() {
if (resetCookieLock) {
console.log('resetCookie 已被锁定')
return
}
resetCookieLock = true
try {
let cookieJar = new tough.CookieJar()
let cookieAxios = axios.create({
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:92.0) Gecko/20100101 Firefox/92.0'
},
timeout: 6000,
maxRedirects: 10
})
axiosCookieJarSupport(cookieAxios)
cookieAxios.defaults.jar = cookieJar
cookieAxios.defaults.withCredentials = true
await cookieAxios.get('http://bkzhjx.wh.sdu.edu.cn/sso.jsp')
let casUrl = 'https://pass.sdu.edu.cn/cas/login?service=http%3A%2F%2Fbkzhjx.wh.sdu.edu.cn%2Fsso.jsp'
let resp = await cookieAxios.get(casUrl)
let ticket = /name="lt" value="(.*?)"/.exec(resp.data)?.[1] || null
let rsa = strEnc(appConfig.username + appConfig.password + ticket, '1', '2', '3')
resp = await cookieAxios.post(casUrl, qs.stringify({
rsa,
ul: appConfig.username.length,
pl: appConfig.password.length,
lt: ticket,
execution: 'e1s1',
_eventId: 'submit'
}))
let nextUrl = 'https://bkzhjx.wh.sdu.edu.cn' + /<a href="(.*?)">进入选课/.exec(resp.data)![1]
resp = await cookieAxios.get(nextUrl)
let xkId = /jrxk\('1','(.*?)'/.exec(resp.data)![1]
nextUrl = 'https://bkzhjx.wh.sdu.edu.cn/jsxsd/xsxk/newXsxkzx?jx0502zbid=' + xkId
await cookieAxios.get(nextUrl)
await cookieAxios.get('https://bkzhjx.wh.sdu.edu.cn/jsxsd/xsxk/selectBottom?jx0502zbid=' + xkId)
let cookieString = cookieJar.getCookieStringSync('https://bkzhjx.wh.sdu.edu.cn/jsxsd/xsxk')
console.log('[cookie]取得Cookie:' + cookieString)
appConfig.cookie = cookieString
sduAxios.defaults.headers['Cookie'] = cookieString
} catch (e) {
throw e
} finally {
resetCookieLock = false
}
}
export function countStr(str: string, word: string) {
return str.split(word).length - 1
}
export {sduAxios}

查看文件

@ -1,9 +1,14 @@
import {JsonCourseList, poll} from "./poll"
import {appConfig} from "./config"
import {acquireProcess, monitProcess, replaceProcess} from "./actions"
import {sleep} from "./includes"
import {resetCookie, sleep} from "./includes"
import {health} from "./health"
import * as fs from "fs"
async function start() {
fs.writeFileSync('resetCookieRequest.txt','0')
console.log('开始获取Cookie')
await resetCookie()
console.log('开始启动轮询进程')
poll()
while (true) {
@ -19,17 +24,23 @@ async function start() {
await sleep(50)
}
console.log('轮询进程启动完毕')
for (let channel of appConfig.channels) {
console.log('channel: ' + channel + ': ' + JsonCourseList[channel].length)
}
console.log('开始启动用户进程')
for (let course of appConfig.monit?.list || []) {
monitProcess(course)
}
for (let course of appConfig.acquire?.list || []) {
console.log('添加抢课课程:' + course.kch + ' ' + course.kxh)
acquireProcess(course)
}
if (appConfig.replace) {
console.log('添加换课课程:' + appConfig.replace.exit.kch + ' ' + appConfig.replace.exit.kxh)
replaceProcess(appConfig.replace.list, appConfig.replace.exit)
}
console.log('用户进程启动完毕')
health()
}
start()

查看文件

@ -1,30 +1,13 @@
import {IJsonCourse} from "./types"
import {getTimeNow, sduAxios, sleep} from "./includes"
import {getTimeNow, logAndNotifyUser, resetCookie, sduAxios, sleep} from "./includes"
import {appConfig} from "./config"
import * as qs from "qs"
import * as fs from "fs"
export let reqBody = 'sEcho=1&iColumns=15&sColumns=&iDisplayStart=0&iDisplayLength=2000&mDataProp_0=kch&mDataProp_1=kcmc&mDataProp_2=kxhnew&mDataProp_3=jkfs&mDataProp_4=xmmc&mDataProp_5=fzmc&mDataProp_6=ktmc&mDataProp_7=xf&mDataProp_8=skls&mDataProp_9=sksj&mDataProp_10=skdd&mDataProp_11=xqmc&mDataProp_12=syrs&mDataProp_13=ctsm&mDataProp_14=czOper'
let currentErrorCount = 0
let totalErrorCount = 0
let reqData = [
{"name": "sEcho", "value": "1"},
{"name": "iColumns", "value": "2000"},// 1
{"name": "sColumns", "value": ""},
{"name": "iDisplayStart", "value": "0"},
{"name": "iDisplayLength", "value": "2000"},// 1
{"name": "mDataProp_0", "value": "kch"},
{"name": "mDataProp_1", "value": "kcmc"},
{"name": "mDataProp_2", "value": "kxhnew"},
{"name": "mDataProp_3", "value": "jkfs"},
{"name": "mDataProp_4", "value": "xmmc"},
{"name": "mDataProp_5", "value": "xf"},
{"name": "mDataProp_6", "value": "skls"},
{"name": "mDataProp_7", "value": "sksj"},
{"name": "mDataProp_8", "value": "skdd"},
{"name": "mDataProp_9", "value": "xqmc"},
{"name": "mDataProp_10", "value": "xkrs"},
{"name": "mDataProp_11", "value": "syrs"},
{"name": "mDataProp_12", "value": "ctsm"},
{"name": "mDataProp_13", "value": "szkcflmc"},
{"name": "mDataProp_14", "value": "czOper"}
]
export const JsonCourseList = <{ bx: IJsonCourse[], xx: IJsonCourse[], rx: IJsonCourse[] }>{
bx: [],
xx: [],
@ -33,36 +16,51 @@ export const JsonCourseList = <{ bx: IJsonCourse[], xx: IJsonCourse[], rx: IJson
async function updateBx() {
try {
let resp = await sduAxios.post('/jsxsd/xsxkkc/xsxkBxxk?1=1&kcxx=&skls=&skfs=', qs.stringify(reqData))
JsonCourseList.bx.length = 0
JsonCourseList.bx.push(...resp.data.aaData)
console.log('更新必修JsonList成功')
} catch (e) {
let resp = await sduAxios.post('/jsxsd/xsxkkc/xsxkBxxk?1=1&kcxx=&skls=&skfs=&xqid=', reqBody)
if (resp.data.aaData.length) {
JsonCourseList.bx.length = 0
JsonCourseList.bx.push(...resp.data.aaData)
} else {
throw new Error(resp.data)
}
} catch (_e) {
console.log('获取必修JsonList失败 at ' + getTimeNow())
await processError(_e as Error)
}
}
async function updateXx() {
try {
let resp = await sduAxios.post('/jsxsd/xsxkkc/xsxkXxxk?1=1&kcxx=&skls=&skfs=', qs.stringify(reqData))
JsonCourseList.xx.length = 0
JsonCourseList.xx.push(...resp.data.aaData)
} catch (e) {
let resp = await sduAxios.post('/jsxsd/xsxkkc/xsxkXxxk?1=1&kcxx=&skls=&skfs=&xqid=', reqBody)
if (resp.data.aaData.length) {
JsonCourseList.xx.length = 0
JsonCourseList.xx.push(...resp.data.aaData)
} else {
throw new Error(resp.data)
}
} catch (_e) {
console.log('获取限选JsonList失败 at ' + getTimeNow())
await processError(_e as Error)
}
}
async function updateRx() {
try {
let resp = await sduAxios.post('/jsxsd/xsxkkc/xsxkGgxxkxk?kcxx=&skls=&skxq=&skjc=&sfym=false&sfct=false&szjylb=&sfxx=false&skfs=', qs.stringify(reqData))
JsonCourseList.rx.length = 0
JsonCourseList.rx.push(...resp.data.aaData)
} catch (e) {
let resp = await sduAxios.post('/jsxsd/xsxkkc/xsxkGgxxkxk?kcxx=&skls=&skxq=&skjc=&sfym=false&sfct=false&szjylb=&sfxx=true&skfs=&xqid=', reqBody)
if (resp.data.aaData.length) {
JsonCourseList.rx.length = 0
JsonCourseList.rx.push(...resp.data.aaData)
} else {
throw new Error(resp.data)
}
} catch (_e) {
console.log('获取任选JsonList失败 at ' + getTimeNow())
await processError(_e as Error)
}
}
export async function poll() {
await checkResetCookieRequest()
if (appConfig.channels.includes('bx')) {
await updateBx()
await sleep(appConfig.interval)
@ -77,3 +75,36 @@ export async function poll() {
}
setImmediate(poll)
}
async function checkResetCookieRequest() {
let req = parseInt(fs.readFileSync('resetCookieRequest.txt').toString())
if (req) {
try {
await resetCookie()
logAndNotifyUser('[resetCookie]成功')
fs.writeFileSync('resetCookieRequest.txt', '0')
} catch (e) {
logAndNotifyUser('[resetCookie]失败')
}
}
}
async function processError(e: Error) {
currentErrorCount++
if (currentErrorCount >= 30) {
console.log('[error]出现轮询error过多,进行重新登录。上一次异常信息:' + e.message)
try {
await resetCookie()
currentErrorCount = 0
} catch (_e) {
console.log('[error]重新登录失败')
}
if (totalErrorCount != -1) {
totalErrorCount++
}
}
if (totalErrorCount >= 10) {
logAndNotifyUser('[error]重新登录失败次数过多,请检查 at ' + getTimeNow())
totalErrorCount = -1
}
}

查看文件

@ -0,0 +1,4 @@
import * as fs from "fs"
fs.writeFileSync('resetCookieRequest.txt','1')
console.log('已请求resetCookie')

查看文件

@ -1,15 +1,55 @@
import {appConfig} from "./config"
import {countStr, logAndNotifyUser, sduAxios, sleep} from "./includes"
import {reqBody} from "./poll"
import * as fs from "fs"
import {IJsonCourse} from "./types"
console.log('channels: ' + appConfig.channels)
for (let course of appConfig.monit?.list || []) {
console.log('添加监控课程 ' + course.kch + ' on channel ' + course.channel)
}
for (let course of appConfig.acquire?.list || []) {
console.log('添加抢课课程 ' + course.kch + ' on channel ' + course.channel)
}
if (appConfig.replace) {
console.log('添加退课课程 ' + appConfig.replace.exit.kch + ' on channel ' + appConfig.replace.exit.channel)
for (let course of appConfig.replace.list) {
console.log('添加换课课程 ' + course.kch + ' on channel ' + course.channel)
async function test() {
console.log('channels: ' + appConfig.channels)
for (let course of appConfig.monit?.list || []) {
console.log('添加监控课程 ' + course.kch + ' on channel ' + course.channel)
}
for (let course of appConfig.acquire?.list || []) {
console.log('添加抢课课程 ' + course.kch + ' on channel ' + course.channel)
}
if (appConfig.replace) {
console.log('添加退课课程 ' + appConfig.replace.exit.kch + ' on channel ' + appConfig.replace.exit.channel)
for (let course of appConfig.replace.list) {
console.log('添加换课课程 ' + course.kch + ' on channel ' + course.channel)
}
}
// sduAxios.defaults.headers['Cookie'] = 'bzb_jsxsd=91B210AA0E876E6C973D8CD52DBB9A5C; bzb_njw=705AF60821F8C9A23CB373F2A0369B85; SERVERID=121'
// let resp = await sduAxios.post('/jsxsd/xsxkkc/xsxkGgxxkxk?kcxx=&skls=&skxq=&skjc=&sfym=false&sfct=false&szjylb=&sfxx=true&skfs=&xqid=', reqBody)
// console.log(resp.data)
// fs.writeFileSync('arr.json', JSON.stringify(resp.data.aaData, null, 2))
// let courseList: IJsonCourse[] = resp.data.aaData
// courseList = courseList.filter(single => single.kcmc.includes('稷下创新')
// && single.xf >= 2)
// for (let course of courseList) {
// console.log(course.kcmc + ' ' + course.kch + ' ' + course.kxh + ' ' + course.syrs + ' ' + course.xf)
// }
// let lastData = ''
// let first=true
// let count=0
// while (true) {
// let resp = await sduAxios.get("jsxsd/xsxkjg/comeXkjglb?isktx=true", {
// headers: {
// 'Cookie': 'bzb_jsxsd=329BCA4E7D8E2421309B14C02587784F; bzb_njw=55FFC1D9D9B2EF5A10D3F28BEC294AE1; SERVERID=123'
// }
// })
// if (resp.data !== lastData && !first) {
// logAndNotifyUser('可以抽签了')
// first=false
// break
// }
// lastData = resp.data
// console.log('rotate '+count)
// count++
// await sleep(1000)
// }
let str=fs.readFileSync('backup/2.html')
console.log(countStr(JSON.stringify(str.toString()),'cqOper'))
}
test()

查看文件

@ -1,3 +1,6 @@
import {scalarOptions} from "yaml"
import Str = scalarOptions.Str
export type TChannel = 'bx' | 'xx' | 'rx'
export interface ICourse {
@ -7,8 +10,13 @@ export interface ICourse {
}
export interface IAppConfig {
username: string,
password: string,
cookie: string,
qmsgKey: string,
tgApiDomain: string,
tgApiKey: string,
tgChatId: number,
interval: number,
channels: TChannel[],
monit?: {