Greasy Fork is available in English.

SEU研究生讲座预约助手

能够在PC端SEU网上办事服务大厅的研究生素质讲座实现自动定时抢讲座,可以做到自动或者手动输入验证码,解放双手!

// ==UserScript==
// @name         SEU研究生讲座预约助手
// @icon         https://i.seu.edu.cn/oss/iconLab/2023-02-17/研究生素质讲座-学生-1076102412478222336.png?sign=rl70_oAzOdwUO0mO4Qq3_eYf5rD-ymKu0Ypgh9DiRnFlT3ds7c8wOok37-UxVig5
// @namespace    http://tampermonkey.net/
// @version      1.02
// @description  能够在PC端SEU网上办事服务大厅的研究生素质讲座实现自动定时抢讲座,可以做到自动或者手动输入验证码,解放双手!
// @author       SEU-xfg
// @match        http://ehall.seu.edu.cn/gsapp/sys/jzxxtjapp/*
// @homepageURL https://github.com/Cliencer/SEU-lecture-helper
// @supportURL  https://github.com/Cliencer/SEU-lecture-helper/issues
// ==/UserScript==

(function () {
    'use strict';
    var settingsPanel = document.createElement('div');
    settingsPanel.innerHTML = `
    <div id="mySettingsPanel" style="position: fixed; top: 40px; right: 10px; z-index: 1000; background-color: white; border: 1px solid black; padding: 10px; display: none;">
    <h3>设置</h3>
    <div>
        <label><input type="checkbox" id="autoVerifyCheck"> 自动输入验证码</label>
    </div>
    <div id="credentials" style="display: none;">
        <div>
            <label>用户名:<input type="text" id="username"></label>
        </div>
        <div>
            <label>密码:<input type="password" id="password"></label>
        </div>
        <div>
            <label>SoftID:<input type="text" id="softid"></label>
        </div>
    </div>
    <div>
        <label>延迟时间(秒):<input type="number" id="delayTime" min="0" value="1"></label>
    </div>
    <button id="saveSettings">保存</button>
    <button id="closeSettings">关闭</button>
    <button id="helpButton">帮助</button>
</div>

`
    var autoVerify = localStorage.getItem('autoVerify') === 'true';
    var delayTime = localStorage.getItem('delayTime') || 1;

    var floatButton = document.createElement('button');
    floatButton.textContent = '插件设置';
    floatButton.style.position = 'fixed';
    floatButton.style.right = '5px';
    floatButton.style.bottom = '200px';
    floatButton.style.padding = '10px 20px';
    floatButton.style.backgroundColor = '#0078D7';
    floatButton.style.color = 'white';
    floatButton.style.border = 'none';
    floatButton.style.borderRadius = '5px';
    floatButton.style.cursor = 'pointer';
    floatButton.style.zIndex = '1000';

    // 添加按钮到文档中
    document.body.appendChild(floatButton);
    document.body.appendChild(settingsPanel);

    // 打开设置面板
    floatButton.addEventListener('click', function () {
        document.getElementById('mySettingsPanel').style.display = 'block';
    });

    // 关闭设置面板
    document.getElementById('closeSettings').addEventListener('click', function () {
        document.getElementById('mySettingsPanel').style.display = 'none';
    });

    // 控制显示/隐藏凭证输入框
    document.getElementById('autoVerifyCheck').addEventListener('change', function () {
        document.getElementById('credentials').style.display = this.checked ? 'block' : 'none';
    });

    // 保存设置
    document.getElementById('saveSettings').addEventListener('click', function () {
        autoVerify = document.getElementById('autoVerifyCheck').checked;
        const username = document.getElementById('username').value;
        const password = document.getElementById('password').value;
        const softid = document.getElementById('softid').value;
        delayTime = document.getElementById('delayTime').value;

        localStorage.setItem('autoVerify', autoVerify);
        localStorage.setItem('username', username);
        localStorage.setItem('password', password);
        localStorage.setItem('softid', softid);
        localStorage.setItem('delayTime', delayTime);
        document.getElementById('mySettingsPanel').style.display = 'none';
        alert('设置已保存');
    });

    // 加载保存的设置
    window.onload = function () {
        const username = localStorage.getItem('username');
        const password = localStorage.getItem('password');
        const softid = localStorage.getItem('softid');

        document.getElementById('autoVerifyCheck').checked = autoVerify;
        document.getElementById('username').value = username;
        document.getElementById('password').value = password;
        document.getElementById('softid').value = softid;
        document.getElementById('delayTime').value = delayTime;

        if (autoVerify) {
            document.getElementById('credentials').style.display = 'block';
        }
    };

    // 显示帮助信息
    document.getElementById('helpButton').addEventListener('click', function () {
        alert(`由于脚本需要自动获取验证码图片,并识别验证码,因此选用超级鹰接口服务。其账户配置过程如下:
1. 访问 http://www.chaojiying.com/ ,注册账号,充值1元作为接口费用。
2. 进入个人中心 > 软件ID,申请一个软件ID。
3. 将用户名,密码,软件ID分别复制到设置的三个框内`);
    });






    //讲座系统请求头
    const lecture_headers = {
        'Accept': '*/*',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6',
        'Connection': 'keep-alive',
        'Content-Length': '0',
        'Host': 'ehall.seu.edu.cn',
        'Origin': 'http://ehall.seu.edu.cn',
        'Referer': 'http://ehall.seu.edu.cn/gsapp/sys/yddjzxxtjappseu/*default/index.do',
        'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1',
        'X-Requested-With': 'XMLHttpRequest',
        'Cookie': document.cookie
    }


    var lectureList

    getTargetLecture()

    // 等待特定元素加载完成
    waitForElement('tbody', function (tbody) {
        // 对tbody进行操作
        observeTbody(tbody);
    });

    function waitForElement(selector, callback) {
        const observer = new MutationObserver((mutations, obs) => {
            const element = document.querySelector(selector);
            if (element) {
                callback(element);
                obs.disconnect();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function observeTbody(tbody) {
        const trObserver = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.addedNodes.length > 0) {
                    mutation.addedNodes.forEach((node) => {
                        if (node.nodeName === 'TR') {
                            // 在这里对每个新添加的tr元素执行操作
                            console.log('新的 tr 元素被添加:', node);

                            insertButtonsInTr(node)
                        }
                    });
                }
            });
        });

        // 监控tbody下的子元素变化
        trObserver.observe(tbody, { childList: true });
    }

    function insertButtonsInTr(tr) {
        // 获取第一个td元素
        const firstTd = tr.querySelector('td');
        // 获取第四个td中的span的文本
        const fourthTdSpanText = tr.querySelector('td:nth-child(4) span')?.textContent.trim();

        if (firstTd && fourthTdSpanText) {
            // 创建一个新的按钮元素
            const button = document.createElement('button');
            button.innerText = `预约自动抢课`;
            button.style.marginLeft = '10px'; // 添加一些样式,例如左边距

            // 将按钮添加到td元素中
            firstTd.appendChild(button);

            // 按钮点击事件
            button.addEventListener('click', function () {
                const lecture = findMatchingObject(fourthTdSpanText)
                button.disabled = true;
                console.log(lecture)
                let wid = lecture.WID
                let time = lecture.YYKSSJ
                let timeout = calculateTimeDifference(time) + delayTime * 1000
                if (timeout < 500) {
                    timeout = 500
                } else if (timeout > 3000) {
                    if (autoVerify) {
                        var dialogText = "正在准备抢课,不要关闭,请耐心等待。"
                    } else {
                        dialogText = "你还未设置自动输入验证码,请在倒计时结束前在电脑前准备手动输入!"
                    }
                    var dialogBox = createWindows10StyleDialog(dialogText)
                }

                startCountdown(timeout, button)


                let keepAliveIntervalId = setInterval(() => {
                    keepAlive(wid);
                }, 60 * 1000);
                setTimeout(() => {
                    try { dialogBox.style.display = 'none'; } catch (e) { }
                    rob(wid)
                    // 抢讲座操作完成后,清除保活定时器
                    clearInterval(keepAliveIntervalId);
                }, timeout);

            });
        }
    }


    // 获取目标讲座信息
    function getTargetLecture() {
        fetch('http://ehall.seu.edu.cn/gsapp/sys/yddjzxxtjappseu/modules/hdyy/queryActivityList.do', {
            method: 'POST',
            headers: lecture_headers
        })
            .then(response => {
                if (response.status === 403) {
                    // 处理403错误
                    return fetch("http://ehall.seu.edu.cn/gsapp/sys/yddjzxxtjappseu/*default/index.do#/hdyy")
                        .then(response => {
                            console.log("Loaded web");
                            getTargetLecture();
                        })
                        .catch(error => {
                            console.error("Error loading webpage: ", error);
                        });
                } else {
                    return response.json();
                }
            })
            .then(res => {
                if (res && res.datas && res.datas.hdlbList) {
                    const lectures = res.datas.hdlbList;
                    console.log("讲座列表: ", lectures);
                    lectureList = lectures;
                } else {
                    console.log("获取讲座信息失败");
                }
            })
            .catch(error => {
                console.log("请求出错: ", error);
            });
    }
    function findMatchingObject(searchString) {
        // 遍历列表中的每个对象
        for (const item of lectureList) {
            // 检查对象是否有JZMC属性并比较该属性与搜索字符串
            if (item.JZMC && item.JZMC == searchString) {
                return item; // 找到匹配,返回对象
            }
        }
        return null; // 未找到匹配,返回null
    }
    function parseVerifyCode(imgBase64) {
        var xhr = new XMLHttpRequest();
        xhr.open('POST', 'http://upload.chaojiying.net/Upload/Processing.php', false); // false for synchronous request

        //验证码请求参数
        var verifyCodeParams = {
            user: localStorage.getItem('username') || '',
            pass: localStorage.getItem('password') || '',
            softid: localStorage.getItem('softid') || '',
            codetype: 1902,
            file_base64: imgBase64
        };

        var formData = new FormData();
        for (var key in verifyCodeParams) {
            formData.append(key, verifyCodeParams[key]);
        }

        xhr.setRequestHeader('Connection', 'Keep-Alive');
        xhr.setRequestHeader('User-Agent', 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)');

        xhr.send(formData);

        if (xhr.status === 200) {
            var res = JSON.parse(xhr.responseText);
            if (res.err_no === 0) {
                return res.pic_str;
            } else {
                console.error("解析验证码出错: " + res.err_str);
                return null;
            }
        } else {
            console.error("网络请求失败");
            return null;
        }
    }
    function keepAlive(wid) {
        const url = "http://ehall.seu.edu.cn/gsapp/sys/yddjzxxtjappseu/modules/hdyy/getActivityDetail.do";
        const body = new URLSearchParams({ 'wid': wid });
    
        fetch(url, {
            method: "POST",
            headers: lecture_headers,
            body: body
        })
        .then(response => {
            if (response.ok) {
                return response.json();
            } else {
                throw new Error('网络请求失败');
            }
        })
        .then(res => {
            if (res.code !== 0) {
                console.error('保活失效,请检查cookie!');
            } else {
                console.log('用户身份有效,登录状态保活');
            }
        })
        .catch(error => {
            console.error('请求出错: ', error);
        });
    }
    
    function reserveLecture(wid, verifyCode) {
        const url = 'http://ehall.seu.edu.cn/gsapp/sys/yddjzxxtjappseu/modules/hdyy/addReservation.do';
        const params = new URLSearchParams({
            'wid': wid,
            'vcode': verifyCode
        });
        console.log(params)
        return fetch(url, {
            method: 'POST',
            headers: lecture_headers,
            body: params
        })
        .then(response => response.json())
        .then(res => {
            console.log('预约接口响应数据: ', res);
            if(res.code === 0 && res.datas === 1){
                alert("预约成功!");
            }else{
                alert("预约失败,原因:"+res.msg);
            }
            
        })
        .catch(error => {
            console.error('请求出错: ', error);
        });
    }
    function getLectureVerifyCode(wid) {
        return new Promise((resolve, reject) => {
            const url = "http://ehall.seu.edu.cn/gsapp/sys/yddjzxxtjappseu/modules/hdyy/vcode.do";
    
            fetch(url, {
                method: "GET",
                headers: lecture_headers
            })
            .then(response => {
                if (response.ok) {
                    return response.json();
                } else {
                    throw new Error('网络请求失败');
                }
            })
            .then(res => {
                try {
                    var base64Str = res.datas;
                    // 从响应中提取 base64 部分
                    base64Str = base64Str.substring(base64Str.indexOf("base64,") + 7);
                    resolve(base64Str);
                } catch (error) {
                    reject(error);
                }
            })
            .catch(error => {
                reject(new Error('请求出错: ' + error.message));
            });
        });
    }
    
    function showVerifyCodeDialog(base64Image, callback) {
        // 创建对话框的 HTML
        var dialogHTML = `
        <div id="verifyCodeModal" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 10000; background-color: white; border: 1px solid black; padding: 20px; display: none;">
            <h2>请输入验证码</h2>
            <img id="verifyCodeImage" src="" style="width: 100%; max-width: 300px; height: auto; margin-bottom: 10px;">
            <input type="text" id="verifyCodeInput" style="width: 100%; margin-bottom: 10px;">
            <button id="verifyCodeSubmit">确定</button>
        </div>
    `;

        // 将对话框添加到文档中
        var dialogDiv = document.createElement('div');
        dialogDiv.innerHTML = dialogHTML;
        document.body.appendChild(dialogDiv);

        // 设置图片内容
        var image = document.getElementById('verifyCodeImage');
        image.src = 'data:image/png;base64,' + base64Image;

        // 显示对话框
        var modal = document.getElementById('verifyCodeModal');
        modal.style.display = 'block';

        // 焦点放在输入框
        var input = document.getElementById('verifyCodeInput');
        input.focus();

        // 处理提交
        function submitHandler() {
            var userInput = input.value;
            modal.style.display = 'none'; // 关闭对话框
            input.value = ''; // 清空输入框
            if (typeof callback === "function") {
                callback(userInput); // 调用回调函数,并传入用户输入
            }
        }

        // 绑定按钮点击事件
        document.getElementById('verifyCodeSubmit').onclick = submitHandler;

        // 绑定回车键事件
        input.onkeypress = function (event) {
            if (event.keyCode === 13) { // 13 是回车键的键码
                submitHandler();
            }
        };
    }
    async function rob(wid) {
        console.log("定时预约任务开始, wid: ", wid);
        try {
            // 获取验证码图片
            let verifyCodeImgBase64 = await getLectureVerifyCode(wid);
            // 解析验证码
            let verifyCode = ''
            if (autoVerify) {
                verifyCode = await parseVerifyCode(verifyCodeImgBase64);
                console.log("解析验证码成功: ", verifyCode);
                // 尝试预约讲座
                let res = await reserveLecture(wid, verifyCode);
            } else {
                showVerifyCodeDialog(verifyCodeImgBase64, function (userInput) {
                    verifyCode = userInput;
                    console.log("手动输入验证码: ", verifyCode);
                    // 尝试预约讲座
                    let res = reserveLecture(wid, verifyCode);

                })
            }


        } catch (error) {
            console.error("出错了:", error);
        }
    }
    function calculateTimeDifference(targetTime) {
        // 将目标时间字符串转换为 Date 对象
        var targetDate = new Date(targetTime);
        // 获取当前时间的 Date 对象
        var now = new Date();
        // 计算差值(毫秒为单位)
        var difference = targetDate.getTime() - now.getTime();
        // 返回差值
        return difference;
    }
    function startCountdown(duration, element) {
        let remainingTime = duration;

        // 添加前导零的函数
        function pad(number) {
            return number < 10 ? '0' + number : number;
        }

        // 更新按钮文本的函数
        function updateText() {
            // 计算时、分、秒
            let hours = Math.floor(remainingTime / (1000 * 60 * 60));
            let minutes = Math.floor((remainingTime % (1000 * 60 * 60)) / (1000 * 60));
            let seconds = Math.floor((remainingTime % (1000 * 60)) / 1000);

            // 更新按钮文本
            element.textContent = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`;

            // 减少剩余时间
            remainingTime -= 1000;

            // 如果时间耗尽,清除计时器
            if (remainingTime < 0) {
                clearInterval(intervalButton);
                element.textContent = "正在抢课...";
            }
        }

        // 设置定时器每秒调用 updateText 函数
        let intervalButton = setInterval(updateText, 1000);

        // 立即更新一次文本
        updateText();
    }
    function createWindows10StyleDialog(t) {
        var dialogHTML = `
        <div id="dialogBox" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); display: none; justify-content: center; align-items: center; z-index: 9999;">
            <div id="dialogContent" style="background-color: white; padding: 20px; border-radius: 5px; text-align: center; box-shadow: 0px 0px 10px rgba(0,0,0,0.5);">
                <p style="font-size: 16px; color: #333; margin-bottom: 20px;">${t}</p>
                <div id="loadingSpinner" style="border: 5px solid #f3f3f3; border-top: 5px solid #0078D7; border-radius: 50%; width: 50px; height: 50px; margin: 10px auto; animation: spin 2s linear infinite;"></div>
                <button id="closeDialog" style="padding: 10px 20px; background-color: #0078D7; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px;">关闭</button>
            </div>
        </div>
        <style>
            #dialogBox {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.5);
            display: flex;
            justify-content: center;
            align-items: center;
        }
        #dialogContent {
                background-color: white;
            padding: 20px;
            border-radius: 5px;
            text-align: center;
        }
        #loadingSpinner {
            border: 5px solid #f3f3f3;
            border-top: 5px solid #3498db;
            border-radius: 50%;
            width: 50px;
            height: 50px;
            animation: spin 2s linear infinite;
        }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
        </style>
    `;

        document.body.insertAdjacentHTML('beforeend', dialogHTML);

        var dialogBox = document.getElementById('dialogBox');
        var closeDialog = document.getElementById('closeDialog');

        closeDialog.addEventListener('click', function () {
            dialogBox.style.display = 'none';
        });

        // 显示对话框
        dialogBox.style.display = 'flex';
        return dialogBox
    }
})();