Skip to content

Commit 3005ca0

Browse files
committed
feat: 可以使用 WebVPN 抢课
1 parent 0a41a84 commit 3005ca0

File tree

8 files changed

+372
-63
lines changed

8 files changed

+372
-63
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121

2222
> `MacOS` 下软件未签名, 所以 `MacOS` 下可能提示 `软件已损坏`,请参见[这篇文章](https://www.mac2m.com/article/450/)修复,或自行下载源码进行编译 (见文末的 `手动构建方法`), 或安装 `Windows` 虚拟机使用 `Windows` 版本 (不推荐)
2323
24+
> 偶尔你会发现一个版本号比 `latest` 版本更高的版本, 这是小鸦抢课的测试版. 测试版通常会加入一些新功能, 但没有经过充分测试, 可能会有一些问题, 所以请谨慎使用.
25+
2426
## 1.2 使用教程
2527

2628
<mark style="font-size: 3rem;">首次开启软件会自动显示教程</mark>, 也可以点击右上角的 `?` 图标再次查看教程. 下面是软件内教程提到的一些额外的详细信息 (选读)

catch.go

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,15 @@ import (
99
)
1010

1111
// 抢课模式主函数
12-
func (a *App) CatchCoursePub(speed int, studentID string, password string, courseID []string, classID []string, headless bool) error {
12+
func (a *App) CatchCoursePub(
13+
speed int,
14+
studentID string,
15+
password string,
16+
courseID []string,
17+
classID []string,
18+
headless bool,
19+
useWebVpn bool,
20+
) error {
1321

1422
runtime.EventsEmit(a.ctx, "currentStatus", "开始抢课")
1523

@@ -37,7 +45,7 @@ func (a *App) CatchCoursePub(speed int, studentID string, password string, cours
3745
errCh := make(chan error, 1)
3846

3947
// 为每个课程创建一个协程
40-
for i := 0; i < len(courseID); i++ {
48+
for i := range courseID {
4149
go func(speed int, studentID string, password string, courseID string, classID string) {
4250
// 当前元素
4351
var ele playwright.Locator
@@ -59,8 +67,13 @@ func (a *App) CatchCoursePub(speed int, studentID string, password string, cours
5967
})
6068

6169
// 跳转到登录页面
62-
_, err = page.Goto("https://cas.bnu.edu.cn/cas/login?service=http%3A%2F%2Fzyfw.bnu.edu.cn%2F")
63-
if err != nil { errCh <- err; return }
70+
if useWebVpn {
71+
_, err = page.Goto("https://one.bnu.edu.cn/dcp/forward.action?path=/portal/portal&p=home")
72+
if err != nil { errCh <- err; return }
73+
} else {
74+
_, err = page.Goto("https://cas.bnu.edu.cn/cas/login?service=http%3A%2F%2Fzyfw.bnu.edu.cn%2F")
75+
if err != nil { errCh <- err; return }
76+
}
6477

6578
// 输入学号
6679
ele = page.Locator("#un")
@@ -89,6 +102,40 @@ func (a *App) CatchCoursePub(speed int, studentID string, password string, cours
89102
if err != nil { errCh <- err; return }
90103
}
91104

105+
// 如果是 Web VPN 模式, 则点击 "教务管理系统"
106+
if (useWebVpn) {
107+
// 监听新页面的创建
108+
var newPage playwright.Page
109+
page.Context().On("page", func(p playwright.Page) {
110+
newPage = p
111+
})
112+
// 点击 "教务管理系统"
113+
page.Evaluate(`() => {
114+
const items = document.querySelectorAll('.ml_item_name')
115+
for (const item of items) {
116+
if (item.textContent?.includes('教务管理系统')) {
117+
item.parentElement?.click()
118+
break
119+
}
120+
}
121+
}`)
122+
// 等待加载
123+
page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
124+
State: playwright.LoadStateNetworkidle,
125+
})
126+
// 等待新页面创建
127+
time.Sleep(1 * time.Second)
128+
// 使用新页面
129+
if newPage == nil {
130+
errCh <- fmt.Errorf("未能成功打开教务管理系统页面")
131+
return
132+
}
133+
page = newPage
134+
page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
135+
State: playwright.LoadStateNetworkidle,
136+
})
137+
}
138+
92139
// 点击 "网上选课"
93140
ele = page.Locator("li[data-code=\"JW1304\"]")
94141
err = ele.Click()
@@ -121,6 +168,7 @@ func (a *App) CatchCoursePub(speed int, studentID string, password string, cours
121168
err = ele.Click()
122169
if err != nil { errCh <- err; return }
123170
// 等待加载
171+
time.Sleep(time.Duration(speed) * time.Millisecond)
124172
page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
125173
State: playwright.LoadStateNetworkidle,
126174
})
@@ -210,7 +258,15 @@ func (a *App) CatchCoursePub(speed int, studentID string, password string, cours
210258
return nil
211259
}
212260

213-
func (a *App) CatchCourseMaj(speed int, studentID string, password string, courseID []string, classID []string, headless bool) error {
261+
func (a *App) CatchCourseMaj(
262+
speed int,
263+
studentID string,
264+
password string,
265+
courseID []string,
266+
classID []string,
267+
headless bool,
268+
useWebVpn bool,
269+
) error {
214270

215271
runtime.EventsEmit(a.ctx, "currentStatus", "开始抢课")
216272

@@ -238,7 +294,7 @@ func (a *App) CatchCourseMaj(speed int, studentID string, password string, cours
238294
errCh := make(chan error, 1)
239295

240296
// 为每个课程创建一个协程
241-
for i := 0; i < len(courseID); i++ {
297+
for i := range courseID {
242298
go func(speed int, studentID string, password string, courseID string, classID string) {
243299
// 当前元素
244300
var ele playwright.Locator
@@ -260,8 +316,13 @@ func (a *App) CatchCourseMaj(speed int, studentID string, password string, cours
260316
})
261317

262318
// 跳转到登录页面
263-
_, err = page.Goto("https://cas.bnu.edu.cn/cas/login?service=http%3A%2F%2Fzyfw.bnu.edu.cn%2F")
264-
if err != nil { errCh <- err; return }
319+
if useWebVpn {
320+
_, err = page.Goto("https://one.bnu.edu.cn/dcp/forward.action?path=/portal/portal&p=home")
321+
if err != nil { errCh <- err; return }
322+
} else {
323+
_, err = page.Goto("https://cas.bnu.edu.cn/cas/login?service=http%3A%2F%2Fzyfw.bnu.edu.cn%2F")
324+
if err != nil { errCh <- err; return }
325+
}
265326

266327
// 输入学号
267328
ele = page.Locator("#un")
@@ -290,6 +351,40 @@ func (a *App) CatchCourseMaj(speed int, studentID string, password string, cours
290351
if err != nil { errCh <- err; return }
291352
}
292353

354+
// 如果是 Web VPN 模式, 则点击 "教务管理系统"
355+
if (useWebVpn) {
356+
// 监听新页面的创建
357+
var newPage playwright.Page
358+
page.Context().On("page", func(p playwright.Page) {
359+
newPage = p
360+
})
361+
// 点击 "教务管理系统"
362+
page.Evaluate(`() => {
363+
const items = document.querySelectorAll('.ml_item_name')
364+
for (const item of items) {
365+
if (item.textContent?.includes('教务管理系统')) {
366+
item.parentElement?.click()
367+
break
368+
}
369+
}
370+
}`)
371+
// 等待加载
372+
page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
373+
State: playwright.LoadStateNetworkidle,
374+
})
375+
// 等待新页面创建
376+
time.Sleep(1 * time.Second)
377+
// 使用新页面
378+
if newPage == nil {
379+
errCh <- fmt.Errorf("未能成功打开教务管理系统页面")
380+
return
381+
}
382+
page = newPage
383+
page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
384+
State: playwright.LoadStateNetworkidle,
385+
})
386+
}
387+
293388
// 点击 "网上选课"
294389
ele = page.Locator("li[data-code=\"JW1304\"]")
295390
err = ele.Click()
@@ -322,6 +417,7 @@ func (a *App) CatchCourseMaj(speed int, studentID string, password string, cours
322417
err = ele.Click()
323418
if err != nil { errCh <- err; return }
324419
// 等待加载
420+
time.Sleep(time.Duration(speed) * time.Millisecond)
325421
page.WaitForLoadState(playwright.PageWaitForLoadStateOptions{
326422
State: playwright.LoadStateNetworkidle,
327423
})

frontend/src/components/Content.tsx

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type FormValues = {
2626
speed: number // 存在 localStorage
2727
studentID: string // 存在 localStorage
2828
password: string // 存在 localStorage (如果记住密码)
29+
network: 'webvpn' | 'intranet' // 存在 localStorage
2930
courses: {
3031
courseID: string,
3132
classID: string,
@@ -110,14 +111,15 @@ export function Content() {
110111
} else if (value.mode === 'WatchCourseSync') {
111112
EventsEmit('systemStatus', '单线程蹲课中')
112113
}
114+
113115
// 抢课函数
114116
if ((value.mode === 'WatchCourseSync' || value.mode === 'WatchCourse') && localStorage.getItem('isProtect') === 'yes') {
115117
// 蹲课保护: Promise 被拒绝时, 会自动重试
116-
const autoRetry = async (func: typeof WatchCoursePub | typeof WatchCoursePubSync | typeof WatchCourseMaj | typeof WatchCourseMajSync, speed: number, studentID: string, password: string, courseID: string[], classID: string[], isHeadless: boolean) => {
118+
const autoRetry = async (func: typeof WatchCoursePub | typeof WatchCoursePubSync | typeof WatchCourseMaj | typeof WatchCourseMajSync, speed: number, studentID: string, password: string, courseID: string[], classID: string[], isHeadless: boolean, useWebVpn: boolean) => {
117119
// eslint-disable-next-line no-constant-condition
118120
while (true) {
119121
try {
120-
await func(speed, studentID, password, courseID, classID, isHeadless)
122+
await func(speed, studentID, password, courseID, classID, isHeadless, useWebVpn)
121123
break
122124
} catch (err) {
123125
EventsEmit('currentStatus', `检测到发生错误: ${err}`)
@@ -128,8 +130,8 @@ export function Content() {
128130
// 开始蹲课保护
129131
if (publicCourses.length > 0 && majorCourses.length > 0) {
130132
const res = await Promise.allSettled([
131-
autoRetry(funcs.public[value.mode], value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no'),
132-
autoRetry(funcs.major[value.mode], value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no'),
133+
autoRetry(funcs.public[value.mode], value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn'),
134+
autoRetry(funcs.major[value.mode], value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn'),
133135
])
134136
res.forEach((res) => {
135137
if (res.status === 'rejected') {
@@ -139,16 +141,16 @@ export function Content() {
139141
})
140142
return
141143
} else if (publicCourses.length > 0) {
142-
await autoRetry(funcs.public[value.mode], value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no')
144+
await autoRetry(funcs.public[value.mode], value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn')
143145
} else if (majorCourses.length > 0) {
144-
await autoRetry(funcs.major[value.mode], value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no')
146+
await autoRetry(funcs.major[value.mode], value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn')
145147
}
146148
} else {
147149
// 关闭蹲课保护或抢课
148150
if (publicCourses.length > 0 && majorCourses.length > 0) {
149151
const res = await Promise.allSettled([
150-
funcs.public[value.mode](value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no'),
151-
funcs.major[value.mode](value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no'),
152+
funcs.public[value.mode](value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn'),
153+
funcs.major[value.mode](value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn'),
152154
])
153155
res.forEach((res) => {
154156
if (res.status === 'rejected') {
@@ -158,9 +160,9 @@ export function Content() {
158160
})
159161
return
160162
} else if (publicCourses.length > 0) {
161-
await funcs.public[value.mode](value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no')
163+
await funcs.public[value.mode](value.speed, value.studentID, value.password, publicCourses.map(course => course.courseID), publicCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn')
162164
} else if (majorCourses.length > 0) {
163-
await funcs.major[value.mode](value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no')
165+
await funcs.major[value.mode](value.speed, value.studentID, value.password, majorCourses.map(course => course.courseID), majorCourses.map(course => course.classID), localStorage.getItem('isHeadless') !== 'no', localStorage.getItem('network') === 'webvpn')
164166
}
165167
}
166168
} catch (err) {
@@ -210,6 +212,7 @@ export function Content() {
210212
courseType: localStorage.getItem('courseType') || 'public',
211213
studentID: localStorage.getItem('studentID') || '',
212214
password: localStorage.getItem('password') || '',
215+
network: localStorage.getItem('network') || 'intranet',
213216
}}
214217
onFinish={async value => {
215218
await handleSubmit(browserStatus, systemStatus, { ...value, courses })
@@ -308,6 +311,22 @@ export function Content() {
308311
]}
309312
/>
310313
</Form.Item>
314+
<div className='text-nowrap bg-gray-100 border border-[#d9d9d9] border-e-0 px-3 flex items-center justify-center'>
315+
网络环境
316+
</div>
317+
<Form.Item
318+
noStyle
319+
name='network'
320+
rules={[{ required: true, message: '请选择网络环境' }]}
321+
>
322+
<Select
323+
id='network-select'
324+
options={[
325+
{ label: '校园网', value: 'intranet' },
326+
{ label: 'WebVPN', value: 'webvpn' },
327+
]}
328+
/>
329+
</Form.Item>
311330
<div className='flex items-center justify-center border rounded-e-md border-[#d9d9d9] pl-3 pr-1'>
312331
<Checkbox
313332
id='headless-select'

frontend/src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function Header() {
4141
className='w-full h-full flex items-center justify-start text-sm gap-2 pl-3'
4242
>
4343
<span className='font-bold' data-tg-tour='测试'>小鸦抢课</span>
44-
<Tag id='version' className='m-0 border-rose-950 bg-white leading-none py-[0.15rem] px-[0.3rem]'>2.1.4</Tag>
44+
<Tag id='version' className='m-0 border-rose-950 bg-white leading-none py-[0.15rem] px-[0.3rem]'>2.2.0</Tag>
4545
<Tag id='status' className='m-0 border-rose-950 bg-white leading-none py-[0.15rem] px-[0.3rem]'>{systemStatus}</Tag>
4646
</p>
4747
<button

frontend/src/libs/driver.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function tutorial() {
6262
},
6363
{
6464
element: '#catch-mode',
65-
popover: { title: '抢课模式', description: '这里可以选择抢课模式和设置收否开启蹲课保护' },
65+
popover: { title: '抢课模式', description: '这里可以选择抢课模式, 以及设置是否开启蹲课保护' },
6666
},
6767
{
6868
element: '#catch-mode-select',
@@ -100,6 +100,10 @@ export function tutorial() {
100100
element: '#refresh-select',
101101
popover: { title: '刷新频率', description: '这里可以设置网页的刷新频率, 并不是越快越好, 一般默认的 1 秒即可, 如果想要减少耗电量, 也可以选择 2 秒或 5 秒' },
102102
},
103+
{
104+
element: '#network-select',
105+
popover: { title: '网络环境', description: '一般情况下, 你应该连接校园网进行抢课/蹲课, 这样网络最稳定; 但如果您因故无法连接校园网, 小鸦抢课也可以使用 WebVPN 模式进入数字京师' },
106+
},
103107
{
104108
element: '#headless-select',
105109
popover: { title: '显示浏览器', description: '勾选这个选项后会在抢课时显示浏览器窗口, 以便实时查看抢课情况. 如果勾选的话请不要手动操作打开的浏览器窗口' },
@@ -142,11 +146,11 @@ export function tutorial() {
142146
},
143147
{
144148
element: undefined,
145-
popover: { title: '重要提示', description: '2: 请确认各项信息填写正确、无课程时间冲突、剩余学分足够、使用校园网、网络流畅 (建议去人少的地方抢课)' },
149+
popover: { title: '重要提示', description: '2: 请确认各项信息填写正确、无课程时间冲突、剩余学分足够、尽量使用校园网 (或使用 WebVPN 模式)、网络流畅 (建议去人少的地方抢课)' },
146150
},
147151
{
148152
element: undefined,
149-
popover: { title: '重要提示', description: '3: 抢课和蹲课的成功率都不是百分之百, 请在软件提示结束或成功后手动二次确认选课结果. 同时, 千万不要将本软件作为唯一的选课手段' },
153+
popover: { title: '重要提示', description: '3: 抢课和蹲课的成功率都不是百分之百, 请在软件提示结束或成功后手动二次确认选课结果. 同时, 千万千万千万不要将本软件作为唯一的选课手段' },
150154
},
151155
{
152156
element: undefined,
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
22
// This file is automatically generated. DO NOT EDIT
33

4-
export function CatchCourseMaj(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean):Promise<void>;
4+
export function CatchCourseMaj(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean,arg7:boolean):Promise<void>;
55

6-
export function CatchCoursePub(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean):Promise<void>;
6+
export function CatchCoursePub(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean,arg7:boolean):Promise<void>;
77

88
export function Dialog(arg1:string,arg2:string):Promise<string>;
99

1010
export function InstallBrowser():Promise<void>;
1111

12-
export function WatchCourseMaj(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean):Promise<void>;
12+
export function WatchCourseMaj(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean,arg7:boolean):Promise<void>;
1313

14-
export function WatchCourseMajSync(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean):Promise<void>;
14+
export function WatchCourseMajSync(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean,arg7:boolean):Promise<void>;
1515

16-
export function WatchCoursePub(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean):Promise<void>;
16+
export function WatchCoursePub(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean,arg7:boolean):Promise<void>;
1717

18-
export function WatchCoursePubSync(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean):Promise<void>;
18+
export function WatchCoursePubSync(arg1:number,arg2:string,arg3:string,arg4:Array<string>,arg5:Array<string>,arg6:boolean,arg7:boolean):Promise<void>;

0 commit comments

Comments
 (0)