From 1cb0aadcd47257481efbefb1569afaadf41f0a0c Mon Sep 17 00:00:00 2001 From: RiceXi <100351162+RiceXi@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:35:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=87=8D=E7=99=BB=E5=BD=95=E5=AE=9A=E6=97=B6=E5=99=A8?= =?UTF-8?q?=EF=BC=88Implement=20auto=20relogin=20feature=20with=20timer?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了自动重登录定时器以防止因登陆过期失效而影响程序运行——无法正常提交选课申请(Added auto relogin functionality with a timer that triggers every 2 hours.) --- public/js/main.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/public/js/main.js b/public/js/main.js index 678ac91..b2e64b0 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -6,7 +6,8 @@ let checkIfFull = true let particlesLoaded = false let particlesPaused = false let studentId - +let autoReloginTimer = null +const AUTO_RELOGIN_INTERVAL = 7200000 // 2小时 = 7200000毫秒 // 导轨 !(function () { @@ -73,6 +74,8 @@ var targetList = JSON.parse(localStorage.getItem("DLSF_target")) || [] targetListRender() // 如果 cookie 失效,尝试使用用户名密码登录 if (!await checkCookie()) { buttonSaveUser() } + // 启动自动重新登录定时器 + startAutoRelogin() })() @@ -145,6 +148,37 @@ async function loginUser() { } } +// 启动自动重新登录定时器 +function startAutoRelogin() { + const username = localStorage.getItem("DLSF_username") + const password = localStorage.getItem("DLSF_password") + + // 只有当用户名和密码都存在时才启动定时器 + if (!username || !password) { + console.log("[自动重登录] 用户名或密码未设置,不启动自动重登录") + return + } + + // 避免重复创建定时器 + if (autoReloginTimer) { + clearInterval(autoReloginTimer) + console.log("[自动重登录] 清除旧的定时器") + } + + // 启动新的定时器 + autoReloginTimer = setInterval(async () => { + console.log("[自动重登录] 开始执行自动重新登录...") + const success = await loginUser() + if (success) { + console.log("[自动重登录] 重新登录成功") + } else { + console.log("[自动重登录] 重新登录失败") + } + }, AUTO_RELOGIN_INTERVAL) + + console.log(`[自动重登录] 定时器已启动,间隔 ${AUTO_RELOGIN_INTERVAL / 1000 / 60} 分钟`) +} + // 获取某门课的全部教学班信息 async function getAccData(courseCode, requestKey = "") { // 给并发请求加区分标识,避免不同 worker 互相取消请求 From b63d92d41cd91bb5074249de208e25ceaf02abe0 Mon Sep 17 00:00:00 2001 From: RiceXi Date: Sat, 19 Sep 2026 02:33:16 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=94=B9=E7=94=A8=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E9=A9=B1=E5=8A=A8=E7=9A=84=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去掉固定 2 小时的重登录定时器,改为在请求返回 DLSF_SUCCESS: false(或请求异常)时 才尝试重新登录,并在重登录成功后自动重试原请求 - 抢课链路中的 initACC、initSelCourses、scSubmit 三个接口改用该封装,轮询与提交遇到 登录失效时可以自愈,不必等到下一个轮询周期 - 重登录使用输入框中用户最新填写的用户名密码,并在登录成功后回写,避免用旧凭据登录 - 重登录与 cookie 探测都做单飞(single-flight)处理,多个 worker 同时发现失效只会登录一次 - 增加 30 秒冷却与连续失败上限,避免频繁登录触发 CAS 风控 - 记录登录时间,在检测到失效时打印距上次登录的间隔,便于统计 cookie 实际存活时长 --- public/js/main.js | 176 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 139 insertions(+), 37 deletions(-) diff --git a/public/js/main.js b/public/js/main.js index b2e64b0..841b22f 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -6,8 +6,14 @@ let checkIfFull = true let particlesLoaded = false let particlesPaused = false let studentId -let autoReloginTimer = null -const AUTO_RELOGIN_INTERVAL = 7200000 // 2小时 = 7200000毫秒 + +// 自动重登录相关状态 +let reloginPromise = null // 单飞(single-flight):多个 worker 同时发现登录失效时只会真正登录一次 +let sessionProbePromise = null // 探测 cookie 是否有效时同样做单飞处理 +let lastReloginAttempt = 0 // 上次尝试重新登录的时间戳 +let consecutiveReloginFailures = 0 // 连续登录失败次数 +const RELOGIN_COOLDOWN = 30000 // 两次登录尝试之间的最小间隔,避免频繁登录触发 CAS 风控 +const MAX_RELOGIN_FAILURES = 3 // 连续失败达到该次数后停止自动重登录,改为提醒用户 // 导轨 !(function () { @@ -74,8 +80,6 @@ var targetList = JSON.parse(localStorage.getItem("DLSF_target")) || [] targetListRender() // 如果 cookie 失效,尝试使用用户名密码登录 if (!await checkCookie()) { buttonSaveUser() } - // 启动自动重新登录定时器 - startAutoRelogin() })() @@ -133,50 +137,148 @@ async function buttonSaveUser() { if (await loginUser()) { showMessage("自动登录成功") } else { showMessage("自动登录失败,请检查用户名和密码") } } +// 读取登录用的用户名密码 +// 输入框里是用户当前填写的内容,优先使用,避免用户改了输入框但没保存时仍然拿旧密码去登录 +function getCredentials() { + const usernameInput = document.getElementById("input-cookie-username") + const passwordInput = document.getElementById("input-cookie-password") + const username = ((usernameInput && usernameInput.value) || localStorage.getItem("DLSF_username") || "").trim() + const password = (passwordInput && passwordInput.value) || localStorage.getItem("DLSF_password") || "" + return { username: username, password: password } +} + async function loginUser() { - const result = await api("/dlsf/loginGetToken", { username: localStorage.getItem("DLSF_username"), password: localStorage.getItem("DLSF_password") }) - if (result.DLSF_SUCCESS) { + const credentials = getCredentials() + if (!credentials.username || !credentials.password) { + console.log("[自动重登录] 用户名或密码为空,跳过登录") + return false + } + + const result = await api("/dlsf/loginGetToken", { username: credentials.username, password: credentials.password }) + if (result && result.DLSF_SUCCESS) { document.getElementById("input-cookie-JSESSIONID").value = result.JSESSIONID document.getElementById("input-cookie-array").value = result.array cookie.JSESSIONID = result.JSESSIONID cookie.array = result.array localStorage.setItem("DLSF_cookie", JSON.stringify(cookie)) - checkCookie() + // 记录本次登录使用的凭据与登录时间,登录时间用于统计 cookie 实际存活了多久 + localStorage.setItem("DLSF_username", credentials.username) + localStorage.setItem("DLSF_password", credentials.password) + localStorage.setItem("DLSF_loginTime", String(Date.now())) + consecutiveReloginFailures = 0 + await checkCookie() return true } else { return false } } -// 启动自动重新登录定时器 -function startAutoRelogin() { - const username = localStorage.getItem("DLSF_username") - const password = localStorage.getItem("DLSF_password") - - // 只有当用户名和密码都存在时才启动定时器 - if (!username || !password) { - console.log("[自动重登录] 用户名或密码未设置,不启动自动重登录") - return +// 判断接口返回是否代表登录状态(cookie)已经失效 +// 服务端拿不到教务系统的有效响应时(例如被重定向到登录页导致 JSON 解析失败)会返回 DLSF_SUCCESS: false +function isSessionInvalidResult(result) { + return !result || result.DLSF_SUCCESS === false +} + +// 探测当前 cookie 是否仍然有效,用来排除偶发的网络错误 +function probeSession() { + if (sessionProbePromise) { + return sessionProbePromise } - - // 避免重复创建定时器 - if (autoReloginTimer) { - clearInterval(autoReloginTimer) - console.log("[自动重登录] 清除旧的定时器") - } - - // 启动新的定时器 - autoReloginTimer = setInterval(async () => { - console.log("[自动重登录] 开始执行自动重新登录...") - const success = await loginUser() - if (success) { - console.log("[自动重登录] 重新登录成功") - } else { - console.log("[自动重登录] 重新登录失败") + + sessionProbePromise = (async () => { + try { + // 带上 _ 参数以免和 cookie 面板手动触发的 checkCookie 请求互相取消 + const result = await api("/studentui/initstudinfo", { _: "probe" }) + return !!(result && result.success) + } catch (error) { + return false + } finally { + sessionProbePromise = null + } + })() + + return sessionProbePromise +} + +// 单飞(single-flight)自动重登录:多个 worker 同时发现失效时只会真正登录一次 +function autoRelogin() { + if (reloginPromise) { + return reloginPromise + } + + reloginPromise = (async () => { + try { + const credentials = getCredentials() + if (!credentials.username || !credentials.password) { + console.log("[自动重登录] 未填写用户名或密码,无法自动重登录") + return false + } + + if (consecutiveReloginFailures >= MAX_RELOGIN_FAILURES) { + console.log("[自动重登录] 连续登录失败次数过多,已停止自动重登录") + return false + } + + if (Date.now() - lastReloginAttempt < RELOGIN_COOLDOWN) { + console.log("[自动重登录] 距离上次尝试时间过短,跳过本次重登录") + return false + } + + const loginTime = Number(localStorage.getItem("DLSF_loginTime")) + if (loginTime) { + console.log(`[自动重登录] 距上次登录约 ${Math.round((Date.now() - loginTime) / 60000)} 分钟时检测到登录失效,尝试重新登录...`) + } else { + console.log("[自动重登录] 检测到登录失效,尝试重新登录...") + } + + lastReloginAttempt = Date.now() + const success = await loginUser() + if (success) { + console.log("[自动重登录] 重新登录成功") + showMessage("登录已失效,已自动重新登录") + } else { + consecutiveReloginFailures++ + console.log(`[自动重登录] 重新登录失败(连续 ${consecutiveReloginFailures} 次)`) + if (consecutiveReloginFailures >= MAX_RELOGIN_FAILURES) { + showMessage("自动重新登录多次失败,请检查用户名和密码") + } + } + return success + } finally { + reloginPromise = null } - }, AUTO_RELOGIN_INTERVAL) - - console.log(`[自动重登录] 定时器已启动,间隔 ${AUTO_RELOGIN_INTERVAL / 1000 / 60} 分钟`) + })() + + return reloginPromise +} + +// 请求封装:返回内容疑似登录失效时先二次确认,确认失效后自动重登录并重试原请求 +async function apiWithRelogin(target, params) { + let result + try { + result = await api(target, params) + } catch (error) { + result = null + } + + if (!isSessionInvalidResult(result)) { + return result + } + + // 二次确认:如果只是偶发错误、cookie 其实有效,就不要白白重登录 + if (await probeSession()) { + return result + } + + if (!await autoRelogin()) { + return result + } + + try { + return await api(target, params) + } catch (error) { + return null + } } // 获取某门课的全部教学班信息 @@ -187,7 +289,7 @@ async function getAccData(courseCode, requestKey = "") { params._ = requestKey } - const result = await api("/selectcourse/initACC", params) + const result = await apiWithRelogin("/selectcourse/initACC", params) if (!result || !result.aaData) { return null } @@ -207,7 +309,7 @@ async function getAccLessonByCttId(courseCode, cttId, requestKey = cttId) { async function getSelectedCourseList(requestKey = "") { // 给并发请求加区分标识,避免不同 worker 互相取消请求 const params = requestKey ? { _: requestKey } : undefined - const result = await api("/selectcourse/initSelCourses", params) + const result = await apiWithRelogin("/selectcourse/initSelCourses", params) if (!result || !result.enrollCourses) { return null } @@ -234,7 +336,7 @@ async function cancelCourse(courseCode, classNo) { // 调用选课接口 async function selectCourse(cttId) { - return await api("/selectcourse/scSubmit", { cttId: cttId }) + return await apiWithRelogin("/selectcourse/scSubmit", { cttId: cttId }) } // 换课失败时,尝试把原来的课抢回来