| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- // 文件: JLU_01.js (JLU 吉林大学教务适配)
- // 经 vpn.jlu.edu.cn 网页 VPN 访问"(新)教务管理系统"(金智旧版接口)抓取课表
- // 流程:VPN 登录 -> 进入(新)教务"我的课表"页 -> 触发本脚本
- // ========== 运行时提取 VPN 代理基址 ==========
- // Sangfor webvpn 把目标主机加密成 hex 段放进 URL,该 hex 是【会话相关】的,
- // 不能硬编码。脚本在课表页运行时,从当前页 URL 提取基址,保证会话无关。
- const PAGE_URL = window.location.href;
- const _idx = PAGE_URL.indexOf("/jwapp/sys/");
- if (_idx < 0) {
- AndroidBridge.showToast("请先进入(新)教务的「我的课表」页面再导入");
- throw new Error("未在课表页运行,无法定位 VPN 基址");
- }
- const VPN_BASE = PAGE_URL.substring(0, _idx); // https://vpn.jlu.edu.cn/https/<会话hex>
- const REFERER = PAGE_URL; // 当前课表页 URL 作为 Referer
- // 七个金智接口(均 POST,credentials:include 带 VPN 会话票 cookie)
- const EP_CURRENT_SEM = "/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do"; // 当前学期(空 body)
- const EP_SEMESTER_LIST = "/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do"; // 学期列表(*order=-DM 倒序,手动选择用)
- const EP_SEM_CAL = "/jwapp/sys/wdkb/modules/xskcb/cxxljc.do"; // 学期校历(XN+XQ -> 起始日/总周次)
- const EP_SECTIONS = "/jwapp/sys/wdkb/modules/jshkcb/jc.do"; // 节次时间(空 body)
- const EP_COURSES = "/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do"; // 课表(XNXQDM,不传 SKZC 取全部)
- const EP_CHANGES = "/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do"; // 调课/停课(XNXQDM)
- const EP_UNDETERMINED = "/jwapp/sys/wdkb/modules/xskcb/xswpkc.do"; // 未定时间课程(XNXQDM,仅提醒不导入)
- // ========== 请求工具 ==========
- async function api(path, body) {
- const hasBody = body != null && body !== "";
- const headers = {
- "Accept": "application/json, text/javascript, */*; q=0.01",
- "X-Requested-With": "XMLHttpRequest",
- "Referer": REFERER
- };
- if (hasBody) {
- headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
- }
- const res = await fetch(VPN_BASE + path, {
- method: "POST",
- headers: headers,
- body: hasBody ? body : "",
- credentials: "include"
- });
- return res.json();
- }
- // ========== 解析:周次二进制串 "0000111111111111" -> [5,6,...,16] ==========
- // 长度可变(14/16...),按字符位遍历即可
- function parseWeeksFromSkzc(skzc) {
- const weeks = [];
- const s = skzc || "";
- for (let i = 0; i < s.length; i++) {
- if (s[i] === "1") weeks.push(i + 1);
- }
- return weeks;
- }
- // ========== 解析:单条金智课程 -> 拾光模型 ==========
- function parseSingleCourse(raw) {
- const name = raw.KCM;
- const teacher = raw.SKJS ? raw.SKJS.replace(/\//g, ",").trim() : "未知教师";
- const position = raw.JASMC || "待定"; // JASMC 可能为 null
- const day = parseInt(raw.SKXQ, 10); // 1=周一...7=周日
- const startSection = parseInt(raw.KSJC, 10);
- const endSection = parseInt(raw.JSJC, 10);
- const weeks = parseWeeksFromSkzc(raw.SKZC);
- if (!name || !day || !startSection || !endSection || weeks.length === 0) {
- return null;
- }
- return {
- name, teacher, position, day, startSection, endSection, weeks,
- _kbId: raw.KBID, // 内部字段,供调课匹配用,保存前清除
- _day: day,
- _startSection: startSection,
- _endSection: endSection
- };
- }
- // ========== 解析:节次时间行 -> timeSlots(只取 SFSY==1 启用的节次)==========
- function parseTimeSlots(rows) {
- return (rows || [])
- .filter(r => Number(r.SFSY) === 1)
- .map(r => ({
- number: r.DM,
- startTime: r.KSSJ,
- endTime: r.JSSJ
- }))
- .sort((a, b) => a.number - b.number);
- }
- // ========== 解析:学期校历 -> {startDate, totalWeeks} ==========
- function parseSemesterCalendar(rows) {
- const r = (rows || [])[0];
- if (!r) return null;
- const startDate = r.XQKSRQ ? String(r.XQKSRQ).split(" ")[0] : null; // "2026-03-09"
- const totalWeeks = Number(r.ZZC) || 20; // 18
- return { startDate, totalWeeks };
- }
- // ========== UI:开始提示 ==========
- async function promptUserToStart() {
- const confirmed = await window.AndroidBridgePromise.showAlert(
- "吉林大学课表导入",
- "请确保您已登录吉大 VPN(vpn.jlu.edu.cn),并已进入(新)教务管理系统的「我的课表」页面。\n未停留在课表页可能导致会话失效,无法获取数据。",
- "开始导入"
- );
- if (!confirmed) {
- AndroidBridge.showToast("已取消导入");
- return false;
- }
- return true;
- }
- // ========== 学期选择(默认当前学期,可选其他)==========
- // 假期教务系统可能仍把"当前学期"指向上学期,故保留手动选择其他学期能力
- async function selectSemester() {
- // 1. 尝试获取当前学期作为默认选项
- let current = null;
- try {
- current = await fetchCurrentSemester();
- } catch (e) {
- console.warn("当前学期获取失败,将直接显示学期列表:", e);
- }
- if (current) {
- // 2. 问用户:导入当前学期 / 选择其他
- const choice = await window.AndroidBridgePromise.showSingleSelection(
- "选择学期",
- JSON.stringify([
- `导入当前学期:${current.mc}`,
- "选择其他学期"
- ]),
- 0
- );
- if (choice === null) {
- AndroidBridge.showToast("已取消导入");
- return null;
- }
- if (choice === 0) return current;
- // choice === 1 -> 继续到学期列表
- }
- // 3. 拉取学期列表手动选择
- let rows = [];
- try {
- rows = await fetchSemesterList();
- } catch (e) {
- AndroidBridge.showToast("学期列表获取失败,请检查登录状态");
- console.error("学期列表获取失败:", e);
- return null;
- }
- if (rows.length === 0) {
- AndroidBridge.showToast("未获取到学期列表");
- return null;
- }
- const labels = rows.map(r => r.MC || r.DM);
- const idx = await window.AndroidBridgePromise.showSingleSelection(
- "选择其他学期",
- JSON.stringify(labels),
- 0
- );
- if (idx === null) {
- AndroidBridge.showToast("已取消导入");
- return null;
- }
- return parseSemesterRow(rows[idx]);
- }
- // ========== 数据获取 ==========
- // 解析学期行 -> {xnxqdm, xn, xq, mc}(当前学期与列表选择共用)
- function parseSemesterRow(r) {
- const xnxqdm = r.DM; // "2025-2026-2"
- const parts = xnxqdm.split("-"); // ["2025","2026","2"]
- const xn = parts[0] + "-" + parts[1]; // "2025-2026"
- const xq = parts[2]; // "2"
- return { xnxqdm, xn, xq, mc: r.MC };
- }
- async function fetchCurrentSemester() {
- const res = await api(EP_CURRENT_SEM, ""); // 空 body
- const r = res?.datas?.dqxnxq?.rows?.[0];
- if (!r) return null;
- return parseSemesterRow(r);
- }
- async function fetchSemesterList() {
- const res = await api(EP_SEMESTER_LIST, "*order=-DM"); // 倒序,最新在前
- return res?.datas?.xnxqcx?.rows || [];
- }
- async function fetchSemesterCalendar(xn, xq) {
- const res = await api(EP_SEM_CAL, `XN=${xn}&XQ=${xq}`);
- return parseSemesterCalendar(res?.datas?.cxxljc?.rows);
- }
- async function fetchTimeSlots() {
- const res = await api(EP_SECTIONS, ""); // 空 body
- return parseTimeSlots(res?.datas?.jc?.rows);
- }
- async function fetchCourses(xnxqdm) {
- const res = await api(EP_COURSES, `XNXQDM=${xnxqdm}`); // 不传 SKZC,取全部课程
- const rows = res?.datas?.cxxszhxqkb?.rows || [];
- return rows.map(parseSingleCourse).filter(c => c !== null);
- }
- // ========== 调课/停课(镜像 CTGU,字段同 legacy 金智)==========
- async function fetchCourseChanges(xnxqdm) {
- const res = await api(EP_CHANGES, `XNXQDM=${xnxqdm}&*order=-SQSJ`);
- return res?.datas?.xsdkkc?.rows || [];
- }
- async function fetchUndeterminedCourses(xnxqdm) {
- const res = await api(EP_UNDETERMINED, `XNXQDM=${xnxqdm}`);
- return res?.datas?.xswpkc?.rows || [];
- }
- /**
- * 将调课/停课变更应用到已解析课程列表(就地修改)
- * - 停课:按 KBID+星期+节次 匹配原始课,删除受影响周次(change.SKZC)
- * - 调时间/地点(TKLXDM 01/03):用新时间/周次/师/地 新建一条课程
- * - weeks 清空的课程(全周停课)被过滤掉
- * @returns {{courses: Array, appliedCount: number}}
- */
- function applyCourseChanges(parsedCourses, rawChanges) {
- let appliedCount = 0;
- for (const change of rawChanges) {
- const kbID = change.KBID;
- const originalTeacher = change.YSKJS ? change.YSKJS.replace(/\//g, ",").trim() : "未知教师";
- const weeksToRemove = parseWeeksFromSkzc(change.SKZC);
- let changeApplied = false;
- const affected = parsedCourses.filter(c =>
- c._kbId === kbID &&
- c._day === parseInt(change.SKXQ, 10) &&
- c._startSection === parseInt(change.KSJC, 10) &&
- c._endSection === parseInt(change.JSJC, 10)
- );
- if (affected.length === 0) continue;
- if (weeksToRemove.length > 0) {
- affected.forEach(course => {
- const before = course.weeks.length;
- course.weeks = course.weeks.filter(w => !weeksToRemove.includes(w));
- if (course.weeks.length < before) changeApplied = true;
- });
- }
- const isTimeLocationChange = (change.TKLXDM === "01" || change.TKLXDM === "03");
- if (isTimeLocationChange && change.XSKZC && change.XSKXQ && change.XKSJC && change.XJSJC) {
- const newWeeks = parseWeeksFromSkzc(change.XSKZC);
- if (newWeeks.length > 0) {
- parsedCourses.push({
- name: change.KCM,
- teacher: change.XSKJS ? change.XSKJS.replace(/\//g, ",").trim() : originalTeacher,
- position: change.XJASMC || change.JASMC || "待定",
- day: parseInt(change.XSKXQ, 10),
- startSection: parseInt(change.XKSJC, 10),
- endSection: parseInt(change.XJSJC, 10),
- weeks: newWeeks,
- _kbId: kbID,
- _day: parseInt(change.XSKXQ, 10),
- _startSection: parseInt(change.XKSJC, 10),
- _endSection: parseInt(change.XJSJC, 10)
- });
- changeApplied = true;
- }
- }
- if (changeApplied) appliedCount++;
- }
- return { courses: parsedCourses.filter(c => c.weeks.length > 0), appliedCount };
- }
- /** 保存前清除内部匹配字段 */
- function stripInternalFields(courses) {
- return courses.map(c => {
- delete c._kbId; delete c._day; delete c._startSection; delete c._endSection;
- return c;
- });
- }
- // ========== 保存 ==========
- async function saveAll(courses, timeSlots, startDate, totalWeeks) {
- // 1. 课表配置(起始日 + 总周次,来自学期校历接口)
- const config = {
- semesterStartDate: startDate, // "2026-03-09" 或 null
- semesterTotalWeeks: totalWeeks // 18
- };
- const cfgOk = await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
- if (!cfgOk) {
- AndroidBridge.showToast("课表配置保存失败");
- return false;
- }
- // 2. 节次时间
- const slotOk = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- if (!slotOk) {
- AndroidBridge.showToast("节次时间保存失败");
- return false;
- }
- // 3. 课程
- const courseOk = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
- if (!courseOk) {
- AndroidBridge.showToast("课程数据保存失败");
- return false;
- }
- return true;
- }
- // ========== 主流程 ==========
- async function runImportFlow() {
- AndroidBridge.showToast("吉林大学课表导入启动...");
- try {
- if (!(await promptUserToStart())) return;
- // 1. 选择学期(默认当前学期,可选其他;假期教务可能仍指上学期)
- const sem = await selectSemester();
- if (!sem) return;
- // 2. 学期校历(起始日 + 总周次)
- const cal = await fetchSemesterCalendar(sem.xn, sem.xq);
- const startDate = cal?.startDate || null;
- const totalWeeks = cal?.totalWeeks || 20;
- // 3. 节次时间
- const timeSlots = await fetchTimeSlots();
- // 4. 课程数据
- const courses = await fetchCourses(sem.xnxqdm);
- if (courses.length === 0) {
- AndroidBridge.showToast("该学期未查询到课程数据");
- return;
- }
- // 4.5 调课/停课(防御性:失败或无数据则跳过,不影响已验证的主流程)
- let finalCourses = courses;
- let changeNote = "";
- try {
- const changes = await fetchCourseChanges(sem.xnxqdm);
- if (changes.length === 0) {
- changeNote = "无调课信息";
- } else {
- const result = applyCourseChanges(courses, changes);
- finalCourses = result.courses;
- if (result.appliedCount > 0) {
- changeNote = `已应用${result.appliedCount}条调课`;
- } else {
- // 调课存在但未能自动应用:阻塞提醒用户手动核对
- await window.AndroidBridgePromise.showAlert(
- "调课提示",
- `检测到 ${changes.length} 条调课记录但未能自动应用。已导入原始课表,请对照教务页"调课信息"手动核对,如有出入请联系开发者。`,
- "知道了"
- );
- }
- }
- } catch (e) {
- console.warn("调课处理跳过:", e);
- changeNote = "调课查询异常";
- }
- finalCourses = stripInternalFields(finalCourses);
- // 4.6 未定时间课程(仅提醒不导入;查询失败静默跳过)
- let undetermined = [];
- try {
- undetermined = await fetchUndeterminedCourses(sem.xnxqdm);
- } catch (e) {
- console.warn("未定课程查询跳过:", e);
- }
- // 5. 保存
- AndroidBridge.showToast(`当前学期:${sem.mc},${finalCourses.length} 条课程${changeNote ? "(" + changeNote + ")" : ""},正在导入...`);
- const ok = await saveAll(finalCourses, timeSlots, startDate, totalWeeks);
- if (!ok) return;
- // 5.5 未定时间课程提醒(导入已成功;提醒失败不回滚,仍结束流程)
- if (undetermined.length > 0) {
- try {
- const names = [...new Set(undetermined.map(c => c.KCM).filter(Boolean))].join("、");
- await window.AndroidBridgePromise.showAlert(
- "导入完成",
- `已导入 ${finalCourses.length} 条课程。\n另有 ${undetermined.length} 门课程上课时间暂未确定,未导入:${names}。\n请关注教务通知,时间确定后重新导入。`,
- "知道了"
- );
- } catch (e) {
- console.warn("未定课程提醒失败:", e);
- AndroidBridge.showToast(`导入成功!共 ${finalCourses.length} 条课程。`);
- }
- } else {
- AndroidBridge.showToast(`导入成功!共 ${finalCourses.length} 条课程。`);
- }
- AndroidBridge.notifyTaskCompletion();
- } catch (error) {
- console.error("主流程异常:", error);
- AndroidBridge.showToast("意外错误: " + error.message);
- }
- }
- // 启动
- runImportFlow();
|