北京航空航天大学(北航)研究生选课人数显示

通过直接调用官方API获取数据,并修正任务调度逻辑,确保所有课程都能被正确查询。

// ==UserScript==
// @name         北京航空航天大学(北航)研究生选课人数显示
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  通过直接调用官方API获取数据,并修正任务调度逻辑,确保所有课程都能被正确查询。
// @author       理亏亏
// @match        https://yjsxk.buaa.edu.cn/yjsxkapp/sys/xsxkappbuaa/course.html
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @run-at       document-idle
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .course-info-display { margin-left: 10px; font-weight: bold; font-size: 0.9em; white-space: nowrap; }
        .course-info-display.error { color: red; }
        .course-info-display.success { color: green; }
        .course-info-display.loading { color: gray; }
    `);

    const API_BASE_URL = 'https://yjsxk.buaa.edu.cn/yjsxkapp/sys/xsxkappbuaa/xsxkCourse/loadAllCourseInfo.do';

    let currentSemesterId = '20251';
    const semesterInput = document.querySelector('input[name="xnxq"]#xnxq');
    if (semesterInput && semesterInput.value) {
        currentSemesterId = semesterInput.value;
        console.log(`成功动态获取到当前学期ID: ${currentSemesterId}`);
    } else {
        console.warn(`未能动态获取当前学期ID,将使用后备值: ${currentSemesterId}`);
    }

    const courseQuerier = {
        taskQueue: [],
        isWorking: false,

        // ★ 修复点 1: 移除 this.start()
        addTask(courseCode, fullCourseName, linkElement) {
            this.taskQueue.push({ courseCode, fullCourseName, linkElement });
            displayCourseInfo(linkElement, '排队中...', '', false, true);
            // this.start(); // <-- 移除这一行
        },

        async start() {
            if (this.isWorking || this.taskQueue.length === 0) return;
            this.isWorking = true;

            const tasks = [...this.taskQueue];
            this.taskQueue = [];

            await Promise.all(tasks.map(async (task) => {
                displayCourseInfo(task.linkElement, '查询中...', '', false, true);
                const { capacity, selected } = await this.queryWithAPI(task);

                setTimeout(() => {
                    const isQueryError = selected.includes('失败') || selected.includes('出错') || selected.includes('匹配');
                    displayCourseInfo(task.linkElement, capacity, selected, isQueryError);
                }, 0);
            }));

            console.log("所有查询任务已完成。");
            this.isWorking = false;
        },

        queryWithAPI(task) {
            const { courseCode, fullCourseName } = task;
            const logPrefix = `[${fullCourseName}]`;

            return new Promise((resolve) => {
                const apiUrl = `${API_BASE_URL}?_=${Date.now()}`;
                const formData = new URLSearchParams();
                formData.append('query_keyword', courseCode);
                formData.append('query_xnxq', currentSemesterId);
                formData.append('query_kkyx', '');
                formData.append('query_kksx', '');
                formData.append('fixedAutoSubmitBug', '');
                formData.append('query_jxsjhnkc', '0');
                formData.append('query_jxsfankc', '0');
                formData.append('pageIndex', '1');
                formData.append('pageSize', '20');
                formData.append('sortField', '');
                formData.append('sortOrder', '');

                GM_xmlhttpRequest({
                    method: 'POST',
                    url: apiUrl,
                    headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
                    data: formData.toString(),
                    timeout: 10000,
                    onload: function(response) {
                        try {
                            if (response.status !== 200) throw new Error(`HTTP状态码: ${response.status}`);
                            const result = JSON.parse(response.responseText);
                            const courses = result.datas || [];
                            for (const course of courses) {
                                const reconstructedFullName = `${course.KCDM}-${course.KCMC}(${course.BJMC})`;
                                if (reconstructedFullName === fullCourseName) {
                                    console.log(`成功精确匹配到课程 ${logPrefix} 的信息。`);
                                    resolve({ capacity: course.KXRS, selected: `${course.YXXKJGRS}人` });
                                    return;
                                }
                            }
                            console.warn(`API查询成功,但在结果中未精确匹配到课程 ${logPrefix}。`);
                            resolve({ capacity: 'N/A', selected: '未精确匹配' });
                        } catch (error) {
                            console.error(`解析课程 ${logPrefix} 的API响应失败:`, error, response.responseText);
                            resolve({ capacity: 'N/A', selected: '解析失败' });
                        }
                    },
                    onerror: function(error) {
                        console.error(`请求课程 ${logPrefix} 的API时发生网络错误:`, error);
                        resolve({ capacity: 'N/A', selected: '网络错误' });
                    },
                    ontimeout: function() {
                        console.error(`请求课程 ${logPrefix} 的API超时。`);
                        resolve({ capacity: 'N/A', selected: '请求超时' });
                    }
                });
            });
        }
    };

    function displayCourseInfo(linkElement, capacity, selected, isError = false, isLoading = false) {
        let infoSpan = linkElement.nextElementSibling;
        if (!infoSpan || !infoSpan.classList.contains('course-info-display')) {
            infoSpan = document.createElement('span');
            infoSpan.classList.add('course-info-display');
            linkElement.after(infoSpan);
        }
        infoSpan.classList.remove('error', 'success', 'loading');
        if (isLoading) {
            infoSpan.classList.add('loading');
            infoSpan.innerText = ` (${capacity})`;
        } else if (isError) {
            infoSpan.classList.add('error');
            infoSpan.innerText = ` (${selected})`;
        } else {
            infoSpan.classList.add('success');
            infoSpan.innerText = ` (容量: ${capacity}, 已选: ${selected})`;
        }
    }

    // ★ 修复点 2: 在循环结束后调用 start()
    function processCourses() {
        const courseLinks = document.querySelectorAll('td.yxkc a[role-kcdm]:not(.processed)');
        if (courseLinks.length === 0) return;

        console.log(`找到 ${courseLinks.length} 门新课程,添加到查询队列...`);
        courseLinks.forEach(linkElement => {
            linkElement.classList.add('processed');
            const courseCode = linkElement.getAttribute('role-kcdm');
            const fullCourseName = linkElement.innerText.trim();
            if (courseCode && fullCourseName) {
                courseQuerier.addTask(courseCode, fullCourseName, linkElement);
            }
        });

        // 所有任务都添加完毕后,统一启动一次处理
        courseQuerier.start();
    }

    function setupMutationObserver() {
        const observer = new MutationObserver(processCourses);
        observer.observe(document.body, { childList: true, subtree: true });
        console.log("主页面 MutationObserver 已启动。");
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            processCourses();
            setupMutationObserver();
        });
    } else {
        processCourses();
        setupMutationObserver();
    }
})();