// ==UserScript== // @name 南京大学羽毛球场地预订自动化工具 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 仅供学习使用 // @author 严宇恒 // @license MIT // @match https://ggtypt.nju.edu.cn/venue/home // @grant GM_xmlhttpRequest // @connect ggtypt.nju.edu.cn // @downloadURL https://update.greasyfork.icu/scripts/531587/%E5%8D%97%E4%BA%AC%E5%A4%A7%E5%AD%A6%E7%BE%BD%E6%AF%9B%E7%90%83%E5%9C%BA%E5%9C%B0%E9%A2%84%E8%AE%A2%E8%87%AA%E5%8A%A8%E5%8C%96%E5%B7%A5%E5%85%B7.user.js // @updateURL https://update.greasyfork.icu/scripts/531587/%E5%8D%97%E4%BA%AC%E5%A4%A7%E5%AD%A6%E7%BE%BD%E6%AF%9B%E7%90%83%E5%9C%BA%E5%9C%B0%E9%A2%84%E8%AE%A2%E8%87%AA%E5%8A%A8%E5%8C%96%E5%B7%A5%E5%85%B7.meta.js // ==/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 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/div/div[3]/div[2]/div/div[2]/div[1]/div[2]/div[2]/div[1]"; } else if (place.toLowerCase() === 'b') { hallXpath = "/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[3]/div[2]/div[2]/div[1]"; } else if (place.toLowerCase() === 'c'){ hallXpath = "/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[4]/div[2]/div[2]/div[1]"; } else { hallXpath = "/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[1]/div[2]/div[2]/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/div/div[3]/div/div[3]/div[1]"); appointmentButton.click(); console.log("进入预约流程。"); // 等待服务器时间达到目标时间 await waitForServerTime(targetTime); // 点击对应的场馆入口 let hallElement = await waitForElementByXpath(hallXpath); hallElement.click(); console.log("选择场馆成功。"); // 调整预约日期或时间:轮询页面时间段(XPath 获取页面中当前展示的时段) let testElement = await waitForElementByXpath("/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[3]/div[1]/div/div/div/div/div/table/thead/tr/td[2]/div"); let currentTimeSlot = timeToInt(testElement.textContent.trim()); while (true) { let direction = desiredHour - currentTimeSlot; if (direction >= 0 && direction <= 4) { break; } else if (direction < 0) { // 点击向前调整按钮 let beforeButton = await waitForElementByXpath("/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[3]/div[1]/div/div/div/div/div/table/tbody/tr[13]/td[2]/div/span"); beforeButton.click(); } else { // 点击向后调整按钮 let afterButton = await waitForElementByXpath("/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[3]/div[1]/div/div/div/div/div/table/tbody/tr[13]/td[6]/div/span"); afterButton.click(); } await sleep(300); testElement = await waitForElementByXpath("/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[3]/div[1]/div/div/div/div/div/table/thead/tr/td[2]/div"); currentTimeSlot = timeToInt(testElement.textContent.trim()); } console.log("调整到正确的预约日期/时段。"); // 计算目标单元格位置并点击对应场地 // td 索引:根据当前时段与目标时段差值计算 let tdIndex = (desiredHour - currentTimeSlot + 2); // +2 用于表格定位 let courtXpath = `/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[3]/div[1]/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/div/div[3]/div[2]/div/div[2]/div[4]/label"); agreementElement.click(); console.log("同意预约协议。"); // 确认预约信息 let subscribeElement = await waitForElementByXpath("/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[5]/div/div[2]"); subscribeElement.click(); console.log("确认预约信息。"); // 选择同伴 let partnerElement = await waitForElementByXpath("/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/form/div/div[2]/div/div/label[1]"); partnerElement.click(); console.log("选择同伴。"); // 提交预约 let submitElement = await waitForElementByXpath("/html/body/div[1]/div/div/div[3]/div[2]/div/div[2]/div[1]/div/div[2]"); submitElement.click(); console.log("提交预约。"); await sleep(3600000); // 等待1小时 } catch (err) { console.error("预约失败:", err); alert("预约失败,请查看控制台日志了解详细错误信息。"); } })(); })();