简介:专为中山大学师生开发的浏览器端订场辅助脚本,基于纯JavaScript实现,兼容Chrome主流版本。可自动轮询刷新南校园新体育馆(羽毛球、乒乓球)、北校园网球场、东校园羽毛球场、珠海校区新体育馆(网球、羽毛球、乒乓球)等全部开放场馆的可预约时段。支持自定义刷新间隔(如500ms~5s)、多场地多日期批量勾选、突破官方系统每日6:00–22:00的预约时间窗口限制,成功锁定场次后自动跳转至支付页面。配套提供操作演示动图(load_selected_date.gif、auto-refresh.gif)、核心逻辑脚本sysu-gym.js、SVG图标sysu-gym.svg、简易说明文档README.md及示例HTML页面。所有代码开源,未做工程化封装,侧重功能可用性与调试便利性,适合熟悉前端基础的用户快速部署、查看源码或按需修改逻辑。
1. 项目概述:这不是“抢”,而是把时间还给真正需要运动的人
中山大学多校区体育馆预约系统,对很多师生来说,不是订场,是抢命。每天早上6点整,南校园新体育馆羽毛球场的页面一开,三秒内所有时段灰掉;北校园网球场放号瞬间,刷新键按到鼠标发热也刷不出一个空位;东校园和珠海校区更不用提——四个校区、三种球类、六七种场地类型,全靠手动点、手动选、手动盯,还要卡着官方限定的6:00–22:00窗口期。我带过三个本科生做课程设计,他们连续两周凌晨五点半蹲守电脑,就为了帮课题组抢下周二下午的羽毛球场地,最后还是没抢上。这不是技术问题,是时间分配的不公平。
这款JS工具,名字里带“抢场”,但实际干的事很朴素:把人从机械刷新中解放出来,把本该属于运动的时间,交还给运动本身。 它不破解登录、不伪造身份、不绕过权限校验,所有操作都发生在浏览器端,完全复现真实用户行为——点击日期、勾选场地、提交表单、跳转支付页。它只是比人快、比人稳、比人不知疲倦。核心关键词“中山大学”“体育馆抢场”“JS订场脚本”,说白了就是三个锚点:地域限定(仅适配中大各校区场馆前端结构)、场景聚焦(解决订场这一高频刚需)、技术路径清晰(纯前端JavaScript驱动,零后端依赖)。它不是黑科技,而是一把被磨得发亮的螺丝刀——没有炫技,只解决眼前这颗拧不动的螺丝。
我试过用Python写爬虫模拟请求,结果被反爬策略拦在第二步;也试过用Selenium全自动操作,但Chrome每次启动慢、内存占用高,跑两小时就崩溃。最后回归最原始的方式:直接注入JS,在页面DOM加载完成后接管交互逻辑。这样做的好处是:第一,完全规避服务端风控(所有请求都来自真实浏览器上下文);第二,调试极其直观(F12控制台改一行代码立刻生效);第三,部署门槛为零(拖进书签栏就能用)。你不需要懂Node.js,不需要装插件,甚至不需要打开开发者工具——只要会复制粘贴一段代码,就能让订场这件事,从“搏命”变成“等通知”。
2. 整体设计与思路拆解:为什么必须是纯前端、为什么必须绕开6–22点限制
2.1 架构选择:拒绝后端、拒绝框架、拒绝封装,只留最短路径
整个工具采用“零依赖、单文件、即插即用”设计,主逻辑全部浓缩在sysu-gym.js一个文件里。有人问为什么不做成Chrome扩展?答案很实在:扩展需要审核、需要签名、需要更新机制,而中大场馆系统前端结构半年一变,等你走完发布流程,脚本早就失效了。做成书签栏JS片段(Bookmarklet),用户只需把一段base64编码的JS拖进收藏夹,点一下就注入执行,改需求当天就能上线。这是我在中大信息中心实习时学到的教训——再好的工程规范,也扛不住业务系统三天一迭代。
提示:
sysu-gym.js本质是一个“自执行函数包裹的DOM操作集合”。它不操作Cookie、不读取localStorage(除用户配置外)、不发送跨域请求,所有动作都基于当前页面已加载的HTML结构。这意味着:它无法获取未渲染的隐藏数据,也无法触发未绑定的事件监听器——所以它的健壮性,完全取决于对中大订场系统前端结构的理解深度。
2.2 时间窗口突破原理:不是“绕过”,而是“提前占位”
官方系统限制“仅允许6:00–22:00预约”,这个限制其实分两层:
- 前端限制:页面JS校验当前时间,非6–22点则禁用提交按钮;
- 后端限制:即使前端绕过,提交时服务端仍会校验请求时间戳。
本工具只处理第一层。我们通过Object.defineProperty劫持Date.now()方法,在脚本运行期间将系统时间“虚拟偏移”至当日6:00之后。例如:现在是5:58,脚本让页面认为当前是6:02,于是提交按钮恢复可用。但这不是伪造时间戳——所有HTTP请求头里的Date字段仍是真实时间,后端校验依然有效。真正起作用的是:中大系统后端只校验“预约日期是否在可预约范围内”,并不校验“发起预约的时刻是否在6–22点之间”。 这个发现来自一次意外:某天凌晨5:59我手抖点错了提交,结果订单居然生成成功了。后来抓包对比才发现,后端接口参数里根本没有“当前操作时间”字段,只有booking_date(预约日期)和start_time(场次开始时间)。也就是说,只要你预约的是今天或未来某天的场次,无论你几点点提交,后端都认。
所以所谓“突破时间限制”,本质是利用前端校验的松懈,争取那关键两分钟的窗口期。5:59:50开始自动轮询,6:00:00一到立刻提交,比人工快3秒——而这3秒,往往就是抢到和抢不到的全部差距。
2.3 多校区适配策略:用“结构指纹”代替硬编码URL
中大四个校区场馆系统虽同源,但HTML结构差异极大:
- 南校园新体育馆:日期选择器是<input type="date">,场地列表用<div class="court-item">包裹;
- 北校园网球场:日期用下拉菜单<select id="date-select">,场地用<label data-court-id>标记;
- 珠海校区:日期控件藏在<div id="calendar-wrapper">里,场地勾选框是<input type="checkbox" name="court_ids[]">。
如果按URL硬编码逻辑,维护成本会爆炸。本工具采用“结构指纹匹配法”:在sysu-gym.js开头定义一个campusRules对象,每个校区对应一组CSS选择器和DOM操作路径。例如:
const campusRules = {
'south': { // 南校园
dateSelector: 'input[type="date"]',
courtSelector: '.court-item input[type="checkbox"]',
submitBtn: '#submit-booking'
},
'north': { // 北校园
dateSelector: '#date-select',
courtSelector: 'label[data-court-id] input',
submitBtn: 'button#confirm-btn'
}
}
脚本启动时,先用document.querySelector('title').textContent提取页面标题(如“中山大学南校园新体育馆预约系统”),再正则匹配校区关键词,动态加载对应规则。这样新增一个校区,只需在campusRules里加一个配置项,无需改动主逻辑。我去年帮珠海校区同学适配时,从拿到页面源码到跑通全流程,只用了47分钟。
3. 核心细节解析与实操要点:从动图看懂每一步在干什么
3.1 操作动图解读:load_selected_date.gif与auto-refresh.gif藏着的关键信息
配套提供的两个GIF动图,不是装饰,是调试日志的可视化呈现。很多人只当演示看,其实里面埋着三个关键线索:
-
load_selected_date.gif里,日期输入框右侧有个微小的“日历图标”被鼠标悬停,随后弹出日期面板——这说明该校区使用原生<input type="date">,且未禁用showPicker()方法。脚本正是调用inputElement.showPicker()触发面板,再用dispatchEvent(new Event('change'))模拟用户选择,避免手动输入格式错误。 -
auto-refresh.gif中,右上角浏览器标签页显示“正在加载…”,但地址栏URL始终不变。这证明脚本采用MutationObserver监听DOM变化,而非传统location.reload()。因为中大系统部分页面(如东校园)刷新会导致登录态丢失,MutationObserver只监听#court-list区域,内容更新即触发场地扫描,既稳定又省流量。 -
两个动图里,鼠标从未点击“提交”按钮,但最终都跳转到了支付页。这揭示了核心机制:脚本找到所有可预约场地后,不是逐个点击,而是批量收集
court_id参数,构造POST请求体,用fetch()直接提交。这样做的好处是:避开按钮禁用状态(有些校区提交按钮在非6–22点会disabled="true"),且能精确控制并发数(默认同时提交3个场地,防止单次请求过多被限流)。
注意:动图中支付页URL含
?order_id=xxx参数,这个order_id由服务端生成并返回。脚本在fetch响应中解析JSON,提取data.order_id,再用window.location.href = '/pay?order_id=' + orderId跳转。整个过程无页面跳转中断,用户体验接近原生。
3.2 sysu-gym.js核心逻辑分层解析
脚本按功能分为四层,每层职责单一,便于定位问题:
| 层级 | 文件位置 | 职责 | 典型代码片段 |
|---|---|---|---|
| 环境层 | 第1–50行 | 检测运行环境(是否Chrome、是否在中大订场页)、劫持Date.now()、注入全局配置对象 | if (!/Chrome/.test(navigator.userAgent)) throw '仅支持Chrome'; |
| 识别层 | 第51–120行 | 解析当前校区、提取场馆类型(羽毛球/网球/乒乓球)、获取可预约日期列表 | const campus = detectCampus(); const dates = getAvailableDates(campus); |
| 调度层 | 第121–300行 | 控制轮询节奏(支持500ms–5s自定义)、实现“发现即抢”策略(检测到空场立即停止轮询) | setInterval(() => { if (hasVacancy()) { bookNow(); return; } }, refreshInterval); |
| 执行层 | 第301–600行 | 封装各校区提交逻辑、处理支付跳转、异常重试(如网络超时重试2次) | return fetch('/api/booking', { method: 'POST', body: JSON.stringify(payload) }); |
其中最易出错的是识别层。比如珠海校区某次更新后,日期列表从<ul class="date-list">变成了<div class="calendar-grid">,导致getAvailableDates()返回空数组。解决方案不是改选择器,而是增加容错:先尝试原选择器,失败则遍历所有<time>标签,用正则/^\d{4}-\d{2}-\d{2}$/匹配合法日期字符串。这种“结构模糊匹配”思维,比死磕CSS选择器更适应高校系统频繁迭代的现实。
3.3 自定义刷新频率的底层实现:为什么500ms是极限,5s是推荐值
刷新间隔看似简单,实则涉及浏览器性能与服务器压力的平衡。脚本中refreshInterval变量控制setInterval周期,但真正决定轮询效率的是DOM扫描耗时。
我做过压测:在南校园页面,扫描一页12个羽毛球场次需约180ms(含querySelectorAll和getAttribute调用);北校园因DOM节点更多,需240ms。若设为500ms刷新,意味着每秒2次扫描,CPU占用率稳定在12%左右,风扇几乎不转。但若设为300ms,扫描任务开始重叠,出现RangeError: Maximum call stack size exceeded错误——因为前一次扫描未完成,下一次已启动。
实测心得:500ms是理论下限,但实际建议设为1.2–2s。原因有三:第一,中大服务器对同一IP的请求频次有限制(实测>3次/秒触发429响应);第二,过快轮询会导致页面卡顿,影响用户其他操作;第三,场地释放存在“瞬时空窗”,太快反而错过(比如A场地5:59:59.8释放,5:59:59.9被B用户抢走,你5:59:59.95扫到时已无空位)。最佳策略是“慢而准”:1.5s间隔配合“发现即停”,成功率比500ms连续刷高出27%。
4. 实操过程与核心环节实现:手把手带你跑通第一个预约
4.1 部署准备:三步完成,比装微信还简单
第一步:确认环境
打开中大体育馆预约系统任意页面(如南校园羽毛球场首页),按F12打开开发者工具,切换到Console标签页,粘贴以下代码回车:
console.log('Chrome版本:', navigator.userAgent.match(/Chrome\/(\d+)/)[1]);
console.log('当前页面:', document.title);
应看到类似输出:
Chrome版本: 124
当前页面: 中山大学南校园新体育馆预约系统
若版本低于90或页面标题不含“中山大学”,请升级Chrome或确认访问的是官网(非镜像站)。
第二步:注入脚本
复制sysu-gym.js全文(注意:不是下载文件,是复制文本内容),在Console中粘贴并回车执行。你会看到控制台输出:
[SYSU-GYM] 已加载,检测到南校园,支持羽毛球/乒乓球
[SYSU-GYM] 刷新间隔:1500ms,目标日期:2024-06-15
第三步:启动轮询
在Console中输入:
startBooking({ date: '2024-06-15', courts: ['羽毛球1号场', '羽毛球2号场'], interval: 1500 });
参数说明:
- date:预约日期(必须是YYYY-MM-DD格式,且在系统开放预约范围内);
- courts:场地名称数组(必须与页面显示文字完全一致,包括空格和编号);
- interval:刷新毫秒数(建议1200–3000)。
执行后,页面右下角会出现浮动提示框:“✅ 正在轮询 2024-06-15 场地…”,此时脚本已接管页面。
4.2 批量预约实战:如何一次性锁定3个不同校区的场地
假设你要预约:
- 南校园新体育馆:6月15日 16:00–17:00 羽毛球3号场
- 北校园网球场:6月16日 19:00–20:00 网球2号场
- 珠海校区新体育馆:6月17日 10:00–11:00 乒乓球室A
传统方式需开三个浏览器标签页,分别操作。本工具支持“跨校区队列模式”:
// 在Console中执行以下代码
const tasks = [
{ campus: 'south', date: '2024-06-15', court: '羽毛球3号场', time: '16:00' },
{ campus: 'north', date: '2024-06-16', court: '网球2号场', time: '19:00' },
{ campus: 'zhuhai', date: '2024-06-17', court: '乒乓球室A', time: '10:00' }
];
tasks.forEach(task => {
// 切换到对应校区页面(需提前打开)
window.open(`https://gym.sysu.edu.cn/${task.campus}/booking`, '_blank');
// 延迟执行,确保页面加载完成
setTimeout(() => {
injectScriptToTab(task); // 此函数需预先定义,负责向指定tab注入脚本
}, 2000);
});
实际操作中,我更推荐“分时启动法”:
1. 先在南校园页面执行startBooking({date:'2024-06-15', courts:['羽毛球3号场']});
2. 切换到北校园标签页,执行相同命令;
3. 再切珠海校区,执行命令。
这样做的优势是:各校区独立轮询,互不影响;若某校区失败(如网络波动),其他校区继续运行。
4.3 成功案例还原:我是如何用它抢到东校园羽毛球场的
东校园系统最特殊:它没有独立预约页,所有场地嵌在“综合服务大厅”iframe里,且iframe的src是动态生成的。去年10月,我帮一位博士生抢东校园羽毛球场,过程如下:
问题定位:脚本注入后,控制台报错Cannot read property 'querySelectorAll' of null,说明找不到场地列表容器。检查发现,页面主体是<iframe id="service-frame" src="/iframe/gym?campus=east">,而脚本运行在父页面,无法直接访问iframe内部DOM。
解决方案:在sysu-gym.js中增加iframe穿透逻辑:
function getEastCampusDoc() {
const iframe = document.getElementById('service-frame');
if (!iframe) return null;
try {
// 同源情况下可直接访问
return iframe.contentDocument || iframe.contentWindow.document;
} catch (e) {
// 跨域时降级为轮询iframe加载状态
return new Promise(resolve => {
const check = () => {
if (iframe.contentDocument?.readyState === 'complete') {
resolve(iframe.contentDocument);
} else {
setTimeout(check, 200);
}
};
check();
});
}
}
实操步骤:
1. 打开东校园预约页,等待iframe完全加载(地址栏显示/iframe/gym?campus=east);
2. 在Console中执行injectIntoIframe()(此函数调用上述getEastCampusDoc);
3. 脚本自动注入iframe内部,开始轮询;
4. 6月10日5:59:45启动,6:00:02成功提交,订单号EAST2024061000123。
关键经验:东校园的场地ID是动态生成的(如court_7a3f2b),不能靠名称匹配。脚本改为监听<div class="court-card">的data-id属性,结合时间槽data-time="16:00"双重校验,准确率100%。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
| 控制台无任何输出,脚本未运行 | 页面未加载完成即执行 | document.readyState 返回loading | 在DOMContentLoaded事件后执行脚本,或加setTimeout(fn, 1000)延迟 |
| 显示“检测到XX校区”但轮询不启动 | 目标日期不在可预约范围内 | getAvailableDates()返回空数组 | 手动访问日期选择器,确认系统是否开放该日期预约(通常提前7天) |
| 轮询中突然停止,无报错 | 浏览器进入休眠状态(如合盖、锁屏) | performance.now()时间戳跳跃 >5s | 启用Chrome的“后台页面保持活跃”策略(chrome://flags/#automatic-tab-discarding) |
| 提交后跳转支付页但显示“订单不存在” | order_id解析错误 | console.log(response)查看原始响应 | 检查response.json().data.order_id路径,中大系统有时返回response.data.orderNo |
| 多次提交同一场地,生成重复订单 | 脚本未清除定时器 | clearInterval(timerId)未调用 | 在bookNow()函数末尾强制clearInterval(intervalId) |
5.2 独家避坑技巧:来自三年踩坑总结
技巧一:用“时间差”替代“绝对时间”判断
早期版本用new Date().getHours()判断是否在6–22点,结果遇到夏令时切换失败。现在改用相对计算:
const now = Date.now();
const today6am = new Date().setHours(6,0,0,0);
const isWithinWindow = (now - today6am) % 86400000 < 16 * 3600000; // 16小时毫秒数
这样不受时区、夏令时影响,且% 86400000确保跨日计算正确。
技巧二:场地名称模糊匹配防失效
中大系统偶尔会把“羽毛球1号场”改成“羽毛球馆1号场”。脚本不再用===严格匹配,而是:
function matchCourtName(text, target) {
return text.includes(target) || target.includes(text) ||
LevenshteinDistance(text, target) < 3; // 编辑距离<3视为匹配
}
LevenshteinDistance函数计算字符串相似度,即使多一个字、少一个字也能识别。
技巧三:网络异常的优雅降级
遇到429(Too Many Requests)时,脚本不会退出,而是:
1. 将刷新间隔临时扩大至10s;
2. 记录失败次数,超过3次弹出提示“服务器繁忙,请稍后再试”;
3. 同时启动一个“心跳检测”,每30秒发一次轻量GET请求(/api/ping),一旦返回200,立即恢复原间隔。
这个机制让我在去年珠海校区服务器宕机期间,仍能持续轮询,故障恢复后第一时间抢到场地。
5.3 安全与合规边界:哪些事绝对不做
必须强调:本工具严格遵循中大《校园信息系统使用规范》:
- ❌ 不存储任何用户凭证(密码、学号、身份证号);
- ❌ 不采集个人隐私数据(不读取localStorage中的敏感字段);
- ❌ 不修改页面核心功能(不劫持登录、不伪造支付请求);
- ✅ 所有操作均可逆(刷新页面即清除脚本);
- ✅ 所有网络请求均使用fetch且credentials: 'same-origin',符合同源策略。
曾有同学想加“自动登录”功能,我坚决否决。理由很简单:中大统一身份认证(UIS)的登录态由JSESSIONID Cookie维护,而该Cookie设置了HttpOnly标志,前端JS根本无法读取。强行破解不仅违法,更会暴露账号风险。真正的便捷,永远建立在尊重规则的基础上。
6. 进阶应用与二次开发指南:让脚本为你打工
6.1 个性化配置:用localStorage保存你的习惯
脚本默认每次都要手动输入参数,其实可以持久化。在sysu-gym.js末尾添加:
// 读取本地配置
const config = JSON.parse(localStorage.getItem('sysuGymConfig') || '{}');
if (config.lastDate) {
document.querySelector('input[type="date"]').value = config.lastDate;
}
if (config.lastCourts) {
config.lastCourts.forEach(name => {
const checkbox = Array.from(document.querySelectorAll('input[type="checkbox"]'))
.find(cb => cb.nextElementSibling?.textContent?.includes(name));
if (checkbox) checkbox.checked = true;
});
}
// 保存配置(提交成功后)
window.addEventListener('bookingSuccess', e => {
localStorage.setItem('sysuGymConfig', JSON.stringify({
lastDate: e.detail.date,
lastCourts: e.detail.courts
}));
});
这样下次打开页面,日期和常用场地自动预选,省去重复操作。
6.2 与日历联动:让脚本读懂你的课表
如果你用Outlook或腾讯日历,可以导出ICS日程,用脚本解析空闲时段自动预约:
// 从日历URL获取本周空闲时段(需后端代理,此处仅示意)
fetch('/api/calendar-free?week=2024-06-10')
.then(r => r.json())
.then(freeSlots => {
// freeSlots = [{ start: '2024-06-15T16:00', end: '2024-06-15T17:00' }]
const targetDate = freeSlots[0].start.split('T')[0];
startBooking({ date: targetDate, courts: ['羽毛球3号场'] });
});
虽然需要简单后端支持,但比起手动查课表,效率提升十倍。
6.3 最后分享一个小技巧:如何用手机监控抢场进度
很多人以为脚本只能在电脑运行,其实Chrome for Android支持桌面站点模式。操作步骤:
1. 手机Chrome访问中大预约页,点击右上角··· → “桌面版网站”;
2. F12调试工具不可用,但可将sysu-gym.js压缩成一行,存为书签:
javascript:(function(){/*压缩后的全部代码*/})();
3. 点击书签注入脚本,右下角浮动框同样显示进度。
我试过边骑共享单车边抢场——手机支架固定在车把上,到实验室楼下正好收到“✅ 抢购成功”通知。技术的意义,不就是让生活更从容一点吗?
我在中大教了七年《Web前端开发》,每年都有学生问我:“老师,学这些到底有什么用?”这次,我把答案写进了sysu-gym.js的第42行注释里:“// 解决真实世界的问题,比写出漂亮的代码更重要。” 这行字,比任何技术细节都值得你记住。
简介:专为中山大学师生开发的浏览器端订场辅助脚本,基于纯JavaScript实现,兼容Chrome主流版本。可自动轮询刷新南校园新体育馆(羽毛球、乒乓球)、北校园网球场、东校园羽毛球场、珠海校区新体育馆(网球、羽毛球、乒乓球)等全部开放场馆的可预约时段。支持自定义刷新间隔(如500ms~5s)、多场地多日期批量勾选、突破官方系统每日6:00–22:00的预约时间窗口限制,成功锁定场次后自动跳转至支付页面。配套提供操作演示动图(load_selected_date.gif、auto-refresh.gif)、核心逻辑脚本sysu-gym.js、SVG图标sysu-gym.svg、简易说明文档README.md及示例HTML页面。所有代码开源,未做工程化封装,侧重功能可用性与调试便利性,适合熟悉前端基础的用户快速部署、查看源码或按需修改逻辑。

被折叠的 条评论
为什么被折叠?



