Warning: fopen(/www/sites/update.greasyfork.icu/index/store/forever/029cb1c21b4339a3411e1d2e4c381016.js): failed to open stream: No space left on device in /www/sites/update.greasyfork.icu/index/scriptControl.php on line 65
// ==UserScript==
// @name 南京大学羽毛球场地预订自动化工具(手机版)
// @namespace http://tampermonkey.net/
// @version 1.2
// @description 仅供学习使用
// @author 严宇恒
// @license MIT
// @match https://ggtypt.nju.edu.cn/venue/*
// @grant GM_xmlhttpRequest
// @connect ggtypt.nju.edu.cn
// @downloadURL none
// ==/UserScript==
(function() {
'use strict';
/********** 工具函数 **********/
// 延时等待
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
// 使用选择器等待页面中元素出现(基于 querySelector)
async function waitForElementBySelector(selector, timeout = 10000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = document.querySelector(selector);
if (el) return el;
await sleep(300);
}
throw new Error(`等待元素 ${selector} 超时`);
}
// 使用 XPath 等待页面中元素出现
async function waitForElementByXpath(xpath, timeout = 10000) {
const start = Date.now();
while (Date.now() - start < timeout) {
let result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
if (result.singleNodeValue) return result.singleNodeValue;
await sleep(300);
}
throw new Error(`等待 XPath 元素 ${xpath} 超时`);
}
// 通过 GM_xmlhttpRequest 请求获取服务器时间(响应头中的 Date 字段)
function fetchServerTime() {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "HEAD",
url: "https://ggtypt.nju.edu.cn/venue/venue-reservation/",
onload: function(response) {
let header = response.responseHeaders;
let match = header.match(/Date:\s*(.+)/i);
if (match && match[1]) {
let serverTime = new Date(match[1].trim());
resolve(serverTime);
} else {
reject("无法获取服务器时间");
}
},
onerror: function() {
reject("网络请求失败");
}
});
});
}
// 等待直到服务器时间达到目标时间
async function waitForServerTime(targetTime) {
while (true) {
try {
let serverTime = await fetchServerTime();
console.log(`服务器时间: ${serverTime}`);
if (serverTime >= targetTime) {
console.log("达到预约时间!");
break;
}
} catch (err) {
console.error(err);
}
await sleep(300);
}
}
// 将时间段文本转换为整数(用于比较时段)
function timeToInt(t) {
// 可能的最早时间为上午9点,可能的最晚时间为晚上22点
const mapping = {
'09:00-10:00': 9,
'10:00-11:00': 10,
'11:00-12:00': 11,
'12:00-13:00': 12,
'13:00-14:00': 13,
'14:00-15:00': 14,
'15:00-16:00': 15,
'16:00-17:00': 16,
'17:00-18:00': 17,
'18:00-19:00': 18,
'19:00-20:00': 19,
'20:00-21:00': 20,
'21:00-22:00': 21
};
return mapping[t] || -1;
}
/********** 主逻辑 **********/
(async function main() {
try {
let waitUrl = "https://ggtypt.nju.edu.cn/venue/home";
while (waitUrl !== location.href) {
await sleep(300);
}
console.log("页面已跳转。");
/********** 用户设定 **********/
// 选择场馆
let place = prompt("请选择场馆: A.方肇周 B.四组团 C.鼓楼");
while (place.toLowerCase() !== 'a' && place.toLowerCase() !== 'b' && place.toLowerCase() !== 'c' && place.toLowerCase() !== 'z') { // z是方肇周乒乓球,由于基本没人预约,这里拿来做测试用
alert("无效输入!");
place = prompt("请选择场馆: A.方肇周 B.四组团 C.鼓楼");
}
// 根据选择构造对应场馆的 XPath
let hallXpath = "";
if (place.toLowerCase() === 'a') {
hallXpath = "/html/body/div[1]/div[2]/div/div/div/div/div/div[1]/div[3]/div[1]";
} else if (place.toLowerCase() === 'b') {
hallXpath = "/html/body/div[1]/div[2]/div/div/div/div/div/div[3]/div[3]/div[1]";
} else if (place.toLowerCase() === 'c'){
hallXpath = "/html/body/div[1]/div[2]/div/div/div/div/div/div[4]/div[3]/div[1]";
} else {
hallXpath = "/html/body/div[1]/div[2]/div/div/div/div/div/div[1]/div[3]/div[3]";
}
// 选择时段
let courtHour = prompt("请输入预约时段(例如:17表示17:00-18:00):");
while (isNaN(courtHour) || courtHour < 9 || courtHour > 21) {
alert("无效输入!");
courtHour = prompt("请输入预约时段(例如:17表示17:00-18:00):");
}
const desiredHour = parseInt(courtHour);
// 根据场馆,选择具体场地号
let courtNumber;
if (place.toLowerCase() === 'a') {
courtNumber = prompt("请选择场地: 可输入的场地号有 7 8 9 10 11 12 13 14 15 16 17 18");
while (isNaN(courtNumber) || courtNumber < 7 || courtNumber > 18) {
alert("无效输入!");
courtNumber = prompt("请选择场地: 可输入的场地号有 7 8 9 10 11 12 13 14 15 16 17 18");
}
courtNumber = parseInt(courtNumber) - 6; // 方肇周的场地从7开始编号,故这里减去6。
} else if (place.toLowerCase() === 'b') {
courtNumber = prompt("请选择场地: 可输入的场地号有 1 2 3 4 5 6 7 8 9 10 11 12");
while (isNaN(courtNumber) || courtNumber < 1 || courtNumber > 12) {
alert("无效输入!");
courtNumber = prompt("请选择场地: 可输入的场地号有 1 2 3 4 5 6 7 8 9 10 11 12");
}
courtNumber = parseInt(courtNumber);
} else if (place.toLowerCase() === 'c') {
courtNumber = prompt("请选择场地: 可输入的场地号有 1 2 3 4 5 6 7 8 9(中1) 10(中2) 11(中3) 12(中4)");
while (isNaN(courtNumber) || courtNumber < 1 || courtNumber > 12) {
alert("无效输入!");
courtNumber = prompt("请选择场地: 可输入的场地号有 1 2 3 4 5 6 7 8 9(中1) 10(中2) 11(中3) 12(中4)");
}
courtNumber = parseInt(courtNumber);
} else {
courtNumber = prompt("请选择场地: 可输入的场地号有 1-30");
while (isNaN(courtNumber) || courtNumber < 1 || courtNumber > 30) {
alert("无效输入!");
courtNumber = prompt("请选择场地: 可输入的场地号有 1-30");
}
courtNumber = parseInt(courtNumber);
}
// 设置预约定时目标时间:默认当天 08:00:00,或用户自定义
let useTimer = prompt("是否开启定时?Y/n(如不开启默认08:00:00)");
let targetTime = new Date();
if (useTimer.toLowerCase() === 'y') {
let hour = prompt("请输入预约时的小时(0-23):");
let minute = prompt("请输入预约时的分钟(0-59):");
let second = prompt("请输入预约时的秒数(0-59):");
let millisecond = prompt("请输入预约时的毫秒数(0-999):");
targetTime.setHours(parseInt(hour), parseInt(minute), parseInt(second), parseInt(millisecond));
} else {
targetTime.setHours(8, 0, 0, 0);
}
console.log(`预约目标时间:${targetTime}`);
/********** 自动化操作流程 **********/
// 点击预约入口
let appointmentButton = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div[1]/div[2]/div/div[1]");
appointmentButton.click();
console.log("进入预约流程。");
// 等待服务器时间达到目标时间
await waitForServerTime(targetTime);
// 点击对应的场馆入口
let hallElement = await waitForElementByXpath(hallXpath);
hallElement.click();
console.log("选择场馆成功。");
// 手机版不用轮询页面时间段
// 计算目标单元格位置并点击对应场地
// td 索引:根据当前时段与目标时段差值计算
let testElement = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[4]/div[2]/div/div/div/div/div/table/thead/tr/td[1]/div");
let currentTimeSlot = timeToInt(testElement.textContent.trim());
let tdIndex = (desiredHour - currentTimeSlot + 1); // +1 用于表格定位
let courtXpath = `/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[4]/div[2]/div/div/div/div/div/table/tbody/tr[${courtNumber}]/td[${tdIndex}]/div`;
let courtElement = await waitForElementByXpath(courtXpath);
courtElement.click();
console.log("选择场地成功。");
// 勾选同意预约协议
let agreementElement = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[5]/div[1]/div[1]/label");
agreementElement.click();
console.log("同意预约协议。");
// 确认预约信息
let subscribeElement = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[5]/div[1]/div[2]");
subscribeElement.click();
console.log("确认预约信息。");
// 选择同伴
let partnerEnter = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[1]/div[3]");
partnerEnter.click();
let partnerElement = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[2]/div/div[3]/div[11]/ul/li/div[1]/label/span/input");
partnerElement.click();
let partnerAgree = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[2]/div/div[5]/div");
partnerAgree.click();
console.log("选择同伴。");
// 提交预约
let submitElement = await waitForElementByXpath("/html/body/div[1]/div[2]/div/div/div/div/div[1]/div[2]/div[1]");
submitElement.click();
console.log("提交预约。");
await sleep(3600000); // 等待1小时
} catch (err) {
console.error("预约失败:", err);
alert("预约失败,请查看控制台日志了解详细错误信息。");
}
})();
})();