Greasy Fork

Greasy Fork is available in English.

南京大学羽毛球场地预订自动化工具

仅供学习使用

当前为 2025-04-02 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         南京大学羽毛球场地预订自动化工具
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  仅供学习使用
// @author       严宇恒
// @license      MIT
// @match        https://ggtypt.nju.edu.cn/venue/*
// @grant        GM_xmlhttpRequest
// @connect      ggtypt.nju.edu.cn
// ==/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("预约失败,请查看控制台日志了解详细错误信息。");
        }
    })();
})();