// 文件: school.js // 功能:从大连海事大学教务系统获取课程表,通过桥接 API 导入到拾光课程表 // ---------- 常量配置 ---------- const BASE_URL = "http://jw.xpaas.dlmu.edu.cn"; const ENDPOINTS = { DATA_QUERY: `${BASE_URL}/eams/dataQuery.action?sf_request_type=ajax`, COURSE_TABLE_FOR_STD: `${BASE_URL}/eams/courseTableForStd.action`, COURSE_TABLE: `${BASE_URL}/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`, HOME_EXT: `${BASE_URL}/eams/homeExt.action`, }; const UNIT_COUNT = 10; // 每天的课程节数 // ---------- 全局验证函数 ---------- /** * 验证学年输入格式 * @param {string} input - 用户输入的年份 * @returns {false|string} 验证通过返回 false,否则返回错误信息 */ function validateYearInput(input) { if (/^\d{4}$/.test(input)) { return false; // 验证通过 } return "请输入四位数字的年份(例如 2024)"; } // ---------- 工具函数 ---------- /** * 解析课程数据 * @param {string} jsCode - 包含课程数据的 JavaScript 代码字符串 * @returns {Array} 课程对象数组 */ function parseCourses(jsCode) { /** * 默认在一个 TaskActivity 中只涉及该课程某一天的信息,且课程每节默认为连续 | 参考:(星期&节次解析器)[parseIndices()] */ const unitCount = UNIT_COUNT; // 主正则:捕获 TaskActivity 参数 + 后续所有代码直到下一个 activity const mainRegex = /var\s*teachers\s*=\s*\[([^;]+?)\];(?:[\s\S]*?)activity\s*=\s*new\s*TaskActivity\(([^;]+?)\);([\s\S]*?)(?=(?:varteachers\s*=)|(?:<\/script>))/g; const courses = []; let match; while ((match = mainRegex.exec(jsCode)) !== null) { console.warn(match); // 第1组:教师信息 const teachers = match[1].split("}").map((s) => s.trim()); const teacherNames = extractTeacherNames(teachers); const teachersNameStr = teacherNames.join(); // 第2组:TaskActivity 参数 const args = match[2] .replaceAll(/\.join\(.*?\)/g, "") .split(",") .map((s) => s.trim()); const courseName = extractCourseName(args[3]); const position = stripQuotes(args[5]); const weekStr = stripQuotes(args[6]); // 第3组:后续代码,提取所有 index const followingCode = match[3]; const indexRegex = /index\s*=\s*(\d+(?:\s*\*\s*unitCount\s*\+\s*\d+)?)\s*;/g; const indices = []; let idxMatch; while ((idxMatch = indexRegex.exec(followingCode)) !== null) { indices.push(evalIndex(idxMatch[1], unitCount)); } // 计算时间信息 const timeInfo = parseIndices(indices, unitCount); courses.push({ name: courseName, teacher: teachersNameStr, position: position, day: timeInfo.day, startSection: timeInfo.startSection, endSection: timeInfo.endSection, weeks: parseWeeks(weekStr), isCustomTime: false, }); } return courses; } /** * 计算 index 表达式的值(安全版本) * @param {string} expr - 表达式字符串 * @param {number} unitCount - 每天的课程节数 * @returns {number} 计算结果 */ function evalIndex(expr, unitCount) { // 替换 unitCount 为实际值并移除空格 const cleanExpr = expr.replace(/unitCount/g, unitCount).replace(/\s+/g, ""); // 使用 Function 构造器替代 eval,仅允许数字和基本运算符 try { const fn = new Function("return " + cleanExpr); return fn(); } catch (error) { console.error("计算表达式失败:", expr, error); return 0; } } /** * 星期&节次解析器,根据 indices 计算 day 和 sections * @param {Array} indices - 索引数组 * @param {number} unitCount - 每天的课程节数 * @returns {Object} 包含 day、startSection、endSection 的对象 */ function parseIndices(indices, unitCount) { /** *| index 范围 | 含义 | *| -------- | --------- | *| `0-9` | 周一 第1-10节 | *| `10-19` | 周二 第1-10节 | *| `20-29` | 周三 第1-10节 | *| ... | ... | */ if (indices.length === 0) return { day: 1, startSection: 1, endSection: 1 }; // 所有 index 应该在同一天 const days = [...new Set(indices.map((i) => Math.floor(i / unitCount) + 1))]; const day = days[0]; // 取第一天(理论上应该只有一天) const sections = indices .map((i) => (i % unitCount) + 1) .sort((a, b) => a - b); return { day: day, startSection: sections[0], endSection: sections[sections.length - 1], }; // 默认解析为同天连堂课,所以仅返回一个 SectionModel } /** * 提取教师姓名 * @param {Array} teachers - 教师信息数组 * @returns {Array} 教师姓名数组 */ function extractTeacherNames(teachers) { const teacherNames = []; for (const teacherMsg of teachers) { if (!teacherMsg || teacherMsg.trim().length === 0) continue; const args = teacherMsg.split(","); teacherNames.push( args[1] .replaceAll(/['"]/g, "") .replace(/name\s*:\s*/gi, "") .trim(), ); } return teacherNames; } /** * 提取课程名称 * @param {string} str - 包含课程名称的字符串 * @returns {string} 清理后的课程名称 */ function extractCourseName(str) { return str.replace(/^["']|["']$/g, "").replace(/\([^)]+\)$/, ""); } /** * 去除字符串两端的引号 * @param {string} str - 输入字符串 * @returns {string} 去除引号后的字符串 */ function stripQuotes(str) { return str.replace(/^["']|["']$/g, ""); } /** * 解析周数字符串 * @param {string} weekStr - 周数字符串(如 "011010...") * @returns {Array} 周数数组 */ function parseWeeks(weekStr) { const weeks = []; for (let i = 0; i < weekStr.length; i++) { if (weekStr[i] === "1") weeks.push(i); } return weeks; } /** * 获取时间段配置 * @returns {Array} 时间段数组 */ function getTimeSlots() { return [ { number: 1, startTime: "08:00", endTime: "08:45" }, { number: 2, startTime: "08:50", endTime: "09:35" }, { number: 3, startTime: "10:00", endTime: "10:45" }, { number: 4, startTime: "10:50", endTime: "11:35" }, { number: 5, startTime: "13:30", endTime: "14:15" }, { number: 6, startTime: "14:20", endTime: "15:05" }, { number: 7, startTime: "15:30", endTime: "16:15" }, { number: 8, startTime: "16:20", endTime: "17:05" }, { number: 9, startTime: "18:00", endTime: "18:45" }, { number: 10, startTime: "18:50", endTime: "19:35" }, ]; } /** * 从HTML中解析学期ID * @param {string} html - HTML字符串 * @param {string} schoolYear - 学年,如 "2025-2026" * @param {string} name - 学期序号,如 "1"(上学期) , "2"(下学期) , "3"(小学期) * @returns {number|null} 学期ID,未找到返回null */ function parseSemesterId(html, schoolYear, name) { if (!html || typeof html !== "string") { console.warn("HTML内容为空或格式错误"); return null; } // 对输入值进行转义,防止正则注入 const escapedSchoolYear = schoolYear.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // 匹配: {id:数字,schoolYear:"学年",name:"学期序号"} const pattern = new RegExp( `\\{id:(\\d+),schoolYear:"${escapedSchoolYear}",name:"${escapedName}"\\}`, ); const match = html.match(pattern); if (match) { const id = parseInt(match[1], 10); console.log(`匹配成功: 学年=${schoolYear}, 学期=${name}, ID=${id}`); return id; } console.warn(`未找到匹配: 学年=${schoolYear}, 学期=${name}`); return null; } /** * 从HTML解析学生ids * @param {string} html - HTML字符串 * @returns {string|null} 学生ID,未找到返回null */ function parseStudentIds(html) { if (!html || typeof html !== "string") { console.warn("HTML内容为空或格式错误"); return null; } // 匹配: `bg.form.addInput(form,"ids","待捕获的数字");` const pattern = new RegExp(`bg\\.form\\.addInput\\(form,"ids","(\\d+)"\\);`); const match = html.match(pattern); if (match) { const ids = match[1]; console.log(`匹配成功: ids=${ids}`); return ids; } console.warn(`未找到匹配的ids`); return null; } // ---------- 网络请求 ---------- /** * 通用的 fetch 请求封装 * @param {string} url - 请求 URL * @param {Object} options - fetch 选项 * @returns {Promise} 去除空白符后的 HTML 字符串 */ async function fetchWithCleanup(url, options = {}) { try { const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const html = await response.text(); return html.replace(/\s/g, ""); } catch (error) { console.error("请求失败:", url, error); throw error; } } /** * 获取学期课程安排HTML数据 * @param {Object} options - 请求配置 * @param {string} [options.tagId] - 标签ID (可选) * @param {string} [options.dataType='semesterCalendar'] - 数据类型 * @param {string|number} [options.value] - 值 (可选) * @param {boolean} [options.empty=false] - 空标志 (可选) * @returns {Promise} HTML字符串 */ async function fetchSemesterCalendar(options = {}) { const { tagId = "semesterBar20826294511Semester", dataType = "semesterCalendar", value = "223", empty = false, } = options; const url = ENDPOINTS.DATA_QUERY; // 构建表单数据 const formData = new URLSearchParams(); formData.append("tagId", tagId); formData.append("dataType", dataType); formData.append("value", value); formData.append("empty", empty); return fetchWithCleanup(url, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", Accept: "text/plain, */*; q=0.01", "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8", Origin: BASE_URL, Referer: ENDPOINTS.COURSE_TABLE_FOR_STD, }, body: formData.toString(), credentials: "include", }); } /** * 获取学生课表页面HTML * @returns {Promise} HTML字符串 */ async function fetchCourseTableForStd() { return fetchWithCleanup(ENDPOINTS.COURSE_TABLE_FOR_STD, { method: "GET", headers: { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8", "Accept-Encoding": "gzip, deflate", Referer: ENDPOINTS.HOME_EXT, "Upgrade-Insecure-Requests": "1", Priority: "u=4", }, credentials: "include", }); } /** * 获取课程表数据 * @param {string} semesterId - 学期ID * @param {string} ids - 学生ID * @returns {Promise} HTML字符串 */ async function fetchCourseTableData(semesterId, ids) { const formData = new URLSearchParams(); formData.append("ignoreHead", "1"); formData.append("setting.kind", "std"); formData.append("startWeek", ""); formData.append("project.id", "1"); formData.append("semester.id", semesterId); formData.append("ids", ids); return fetchWithCleanup(ENDPOINTS.COURSE_TABLE, { method: "POST", headers: { Accept: "*/*", "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest", Origin: BASE_URL, Referer: ENDPOINTS.COURSE_TABLE, }, body: formData.toString(), credentials: "include", }); } // ---------- 用户交互 ---------- /** * 提示用户确认开始导入 * @returns {Promise} 用户是否确认 */ async function promptUserToStart() { return await window.AndroidBridgePromise.showAlert( "重要提醒", "请确保您已登录服务大厅,并进入海大教务系统内的任意页面(不用点开课表)。\n值得注意的是,教务课表与海大在线的课表并非完全同步,若后期学校调课不规范,可能导致教务课表滞后。\n\n点击确定继续。", "确定", ); } /** * 获取用户输入的学年 * @returns {Promise} 用户输入的年份,取消返回null */ async function getAcademicYear() { return await window.AndroidBridgePromise.showPrompt( "学年设置", "请输入本学年开始的年份\n(例如 2024,代表 2024-2025 学年)", "2024", "validateYearInput", // 传入验证函数名 ); } /** * 让用户选择学期 * @returns {Promise} 选择的学期索引(0-2),取消返回null */ async function selectSemester() { const semesterOptions = ["上学期", "下学期", "小学期"]; const index = await window.AndroidBridgePromise.showSingleSelection( "选择学期", JSON.stringify(semesterOptions), 0, ); if (index === null || index < 0 || index >= semesterOptions.length) { return null; } return index; } // ---------- 主流程 ---------- /** * 主流程函数:协调整个课程表导入流程 */ async function run() { try { // 1. 公告 const confirmed = await promptUserToStart(); if (!confirmed) { AndroidBridge.showToast("用户取消了导入流程。"); return; } // 2. 获取学年 const yearInput = await getAcademicYear(); if (yearInput === null) { AndroidBridge.showToast("导入已取消。"); return; } const yearNum = parseInt(yearInput); if (isNaN(yearNum) || yearNum <= 2000 || yearNum > 2100) { await window.AndroidBridgePromise.showAlert( "错误", "学年输入无效,请输入2001-2100之间的数字。", "确定", ); return; } const schoolYear = `${yearNum}-${yearNum + 1}`; // 3. 获取学期 const semesterIndex = await selectSemester(); if (semesterIndex === null) { AndroidBridge.showToast("导入已取消。"); return; } const termCode = semesterIndex === 0 ? "1" : semesterIndex === 1 ? "2" : "3"; const semesterHtml = await fetchSemesterCalendar(); const semesterId = parseSemesterId(semesterHtml, schoolYear, termCode); // 4. 请求课表 AndroidBridge.showToast("正在获取课表,请稍候..."); let courseTableDataHtml = ""; try { const idsHtml = await fetchCourseTableForStd(); const ids = parseStudentIds(idsHtml); courseTableDataHtml = await fetchCourseTableData(semesterId, ids); } catch (fetchErr) { await window.AndroidBridgePromise.showAlert( "网络请求失败", `请求教务系统失败:${fetchErr.message}\n\n请检查网络连接和登录状态。`, "确定", ); return; } if (!courseTableDataHtml.length) { await window.AndroidBridgePromise.showAlert( "提示", "未获取到任何课程数据。请确认已登录教务系统并选择正确的学年学期。", "确定", ); return; } // 5. 解析并转换 const targetCourses = parseCourses(courseTableDataHtml); // 6. 保存课程 try { await window.AndroidBridgePromise.saveImportedCourses( JSON.stringify(targetCourses), ); AndroidBridge.showToast( `课程数据已导入(共 ${targetCourses.length} 条)`, ); } catch (saveErr) { await window.AndroidBridgePromise.showAlert( "保存课程失败", saveErr.message, "确定", ); return; } // 7. 保存时间段 const timeSlots = getTimeSlots(); try { await window.AndroidBridgePromise.savePresetTimeSlots( JSON.stringify(timeSlots), ); AndroidBridge.showToast("时间段数据已导入"); } catch (slotErr) { // 时间段保存失败不终止流程,只提示 AndroidBridge.showToast(`时间段保存失败:${slotErr.message}`); } // 8. 完成通知 AndroidBridge.showToast("导入完成!"); AndroidBridge.notifyTaskCompletion(); } catch (err) { // 捕获所有未预料的错误 console.error("run error:", err); await window.AndroidBridgePromise.showAlert( "导入失败", `未知错误:${err.message || err}\n\n请联系开发者。`, "确定", ); // 仍然通知完成,但可能不会生成有效文件 AndroidBridge.notifyTaskCompletion(); } } // 启动 run();