|
@@ -1,20 +1,22 @@
|
|
|
// 文件: JLU_01.js (JLU 吉林大学教务适配)
|
|
// 文件: JLU_01.js (JLU 吉林大学教务适配)
|
|
|
-// 经 vpn.jlu.edu.cn 网页 VPN 访问"(新)教务管理系统"(金智旧版接口)抓取课表
|
|
|
|
|
-// 流程:VPN 登录 -> 进入(新)教务"我的课表"页 -> 触发本脚本
|
|
|
|
|
-
|
|
|
|
|
-// ========== 运行时提取 VPN 代理基址 ==========
|
|
|
|
|
-// Sangfor webvpn 把目标主机加密成 hex 段放进 URL,该 hex 是【会话相关】的,
|
|
|
|
|
-// 不能硬编码。脚本在课表页运行时,从当前页 URL 提取基址,保证会话无关。
|
|
|
|
|
|
|
+// 经 iedu.jlu.edu.cn 直连"(新)教务管理系统"(金智旧版接口)抓取课表
|
|
|
|
|
+// 流程:统一身份认证登录 -> 进入(新)教务"我的课表"页 -> 触发本脚本
|
|
|
|
|
+// 兼容:经 Sangfor 网页 VPN 代理访问时基址可自动适配,无需改动脚本
|
|
|
|
|
+
|
|
|
|
|
+// ========== 运行时提取教务站点基址 ==========
|
|
|
|
|
+// 公网直连时基址为 https://iedu.jlu.edu.cn;若经 Sangfor webvpn 代理,则为
|
|
|
|
|
+// https://vpn.jlu.edu.cn/https/<会话hex>(该 hex 是【会话相关】的,不能硬编码)。
|
|
|
|
|
+// 两种情况都在课表页运行时从当前页 URL 提取基址,保证会话无关。
|
|
|
const PAGE_URL = window.location.href;
|
|
const PAGE_URL = window.location.href;
|
|
|
const _idx = PAGE_URL.indexOf("/jwapp/sys/");
|
|
const _idx = PAGE_URL.indexOf("/jwapp/sys/");
|
|
|
if (_idx < 0) {
|
|
if (_idx < 0) {
|
|
|
window.shiguangBridge.showToast("请先进入(新)教务的「我的课表」页面再导入");
|
|
window.shiguangBridge.showToast("请先进入(新)教务的「我的课表」页面再导入");
|
|
|
- throw new Error("未在课表页运行,无法定位 VPN 基址");
|
|
|
|
|
|
|
+ throw new Error("未在课表页运行,无法定位教务站点基址");
|
|
|
}
|
|
}
|
|
|
-const VPN_BASE = PAGE_URL.substring(0, _idx); // https://vpn.jlu.edu.cn/https/<会话hex>
|
|
|
|
|
|
|
+const SITE_BASE = PAGE_URL.substring(0, _idx); // https://iedu.jlu.edu.cn
|
|
|
const REFERER = PAGE_URL; // 当前课表页 URL 作为 Referer
|
|
const REFERER = PAGE_URL; // 当前课表页 URL 作为 Referer
|
|
|
|
|
|
|
|
-// 七个金智接口(均 POST,credentials:include 带 VPN 会话票 cookie)
|
|
|
|
|
|
|
+// 七个金智接口(均 POST,credentials:include 携带教务站点会话 cookie)
|
|
|
const EP_CURRENT_SEM = "/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do"; // 当前学期(空 body)
|
|
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_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_SEM_CAL = "/jwapp/sys/wdkb/modules/xskcb/cxxljc.do"; // 学期校历(XN+XQ -> 起始日/总周次)
|
|
@@ -34,7 +36,7 @@ async function api(path, body) {
|
|
|
if (hasBody) {
|
|
if (hasBody) {
|
|
|
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
|
}
|
|
}
|
|
|
- const res = await fetch(VPN_BASE + path, {
|
|
|
|
|
|
|
+ const res = await fetch(SITE_BASE + path, {
|
|
|
method: "POST",
|
|
method: "POST",
|
|
|
headers: headers,
|
|
headers: headers,
|
|
|
body: hasBody ? body : "",
|
|
body: hasBody ? body : "",
|
|
@@ -54,10 +56,16 @@ function parseWeeksFromSkzc(skzc) {
|
|
|
return weeks;
|
|
return weeks;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// ========== 解析:教师字段 ==========
|
|
|
|
|
+// 教务格式为 "姓名/工号",多教师之间用 "," 分隔(工号不能当成第二位教师)
|
|
|
|
|
+function cleanTeacher(raw) {
|
|
|
|
|
+ return (raw || "").split(",").map(t => t.split("/")[0].trim()).filter(Boolean).join(",");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// ========== 解析:单条金智课程 -> 拾光模型 ==========
|
|
// ========== 解析:单条金智课程 -> 拾光模型 ==========
|
|
|
function parseSingleCourse(raw) {
|
|
function parseSingleCourse(raw) {
|
|
|
const name = raw.KCM;
|
|
const name = raw.KCM;
|
|
|
- const teacher = raw.SKJS ? raw.SKJS.replace(/\//g, ",").trim() : "未知教师";
|
|
|
|
|
|
|
+ const teacher = cleanTeacher(raw.SKJS) || "未知教师";
|
|
|
const position = raw.JASMC || "待定"; // JASMC 可能为 null
|
|
const position = raw.JASMC || "待定"; // JASMC 可能为 null
|
|
|
const day = parseInt(raw.SKXQ, 10); // 1=周一...7=周日
|
|
const day = parseInt(raw.SKXQ, 10); // 1=周一...7=周日
|
|
|
const startSection = parseInt(raw.KSJC, 10);
|
|
const startSection = parseInt(raw.KSJC, 10);
|
|
@@ -68,10 +76,7 @@ function parseSingleCourse(raw) {
|
|
|
}
|
|
}
|
|
|
return {
|
|
return {
|
|
|
name, teacher, position, day, startSection, endSection, weeks,
|
|
name, teacher, position, day, startSection, endSection, weeks,
|
|
|
- _kbId: raw.KBID, // 内部字段,供调课匹配用,保存前清除
|
|
|
|
|
- _day: day,
|
|
|
|
|
- _startSection: startSection,
|
|
|
|
|
- _endSection: endSection
|
|
|
|
|
|
|
+ _kbId: raw.KBID // 内部字段,调课按 KBID 匹配用,保存前清除
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -100,7 +105,7 @@ function parseSemesterCalendar(rows) {
|
|
|
async function promptUserToStart() {
|
|
async function promptUserToStart() {
|
|
|
const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
"吉林大学课表导入",
|
|
"吉林大学课表导入",
|
|
|
- "请确保您已登录吉大 VPN(vpn.jlu.edu.cn),并已进入(新)教务管理系统的「我的课表」页面。\n未停留在课表页可能导致会话失效,无法获取数据。",
|
|
|
|
|
|
|
+ "请确保您已登录(新)教务管理系统,并已进入「我的课表」页面。\n未停留在课表页可能导致会话失效,无法获取数据。",
|
|
|
"开始导入"
|
|
"开始导入"
|
|
|
);
|
|
);
|
|
|
if (!confirmed) {
|
|
if (!confirmed) {
|
|
@@ -203,7 +208,7 @@ async function fetchCourses(xnxqdm) {
|
|
|
return rows.map(parseSingleCourse).filter(c => c !== null);
|
|
return rows.map(parseSingleCourse).filter(c => c !== null);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// ========== 调课/停课(镜像 CTGU,字段同 legacy 金智)==========
|
|
|
|
|
|
|
+// ========== 调课/停课(xsdkkc.do,字段已按真实抓包核对)==========
|
|
|
async function fetchCourseChanges(xnxqdm) {
|
|
async function fetchCourseChanges(xnxqdm) {
|
|
|
const res = await api(EP_CHANGES, `XNXQDM=${xnxqdm}&*order=-SQSJ`);
|
|
const res = await api(EP_CHANGES, `XNXQDM=${xnxqdm}&*order=-SQSJ`);
|
|
|
return res?.datas?.xsdkkc?.rows || [];
|
|
return res?.datas?.xsdkkc?.rows || [];
|
|
@@ -216,8 +221,8 @@ async function fetchUndeterminedCourses(xnxqdm) {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 将调课/停课变更应用到已解析课程列表(就地修改)
|
|
* 将调课/停课变更应用到已解析课程列表(就地修改)
|
|
|
- * - 停课:按 KBID+星期+节次 匹配原始课,删除受影响周次(change.SKZC)
|
|
|
|
|
- * - 调时间/地点(TKLXDM 01/03):用新时间/周次/师/地 新建一条课程
|
|
|
|
|
|
|
+ * - 停课:按 KBID 匹配原始课,删除受影响周次(change.SKZC)
|
|
|
|
|
+ * - 调课:新时间字段齐全时,按新时间/周次/师/地落位;同一 KBID 已存在该时段则并入周次
|
|
|
* - weeks 清空的课程(全周停课)被过滤掉
|
|
* - weeks 清空的课程(全周停课)被过滤掉
|
|
|
* @returns {{courses: Array, appliedCount: number}}
|
|
* @returns {{courses: Array, appliedCount: number}}
|
|
|
*/
|
|
*/
|
|
@@ -225,45 +230,52 @@ function applyCourseChanges(parsedCourses, rawChanges) {
|
|
|
let appliedCount = 0;
|
|
let appliedCount = 0;
|
|
|
for (const change of rawChanges) {
|
|
for (const change of rawChanges) {
|
|
|
const kbID = change.KBID;
|
|
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;
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // 课表接口同一学期内 KBID 唯一(实测 11/11),按 KBID 即可精确定位
|
|
|
|
|
+ const affected = parsedCourses.filter(c => c._kbId === kbID);
|
|
|
|
|
+ if (affected.length === 0) {
|
|
|
|
|
+ console.warn("调课未匹配到课程, KBID:", kbID);
|
|
|
|
|
+ continue;
|
|
|
}
|
|
}
|
|
|
|
|
+ let changeApplied = false;
|
|
|
|
|
|
|
|
- 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) {
|
|
|
|
|
|
|
+ // 1. 从原课剔除被调走的周次(课表给整学期周次,调课记录只给被调走的那几周)
|
|
|
|
|
+ const weeksToRemove = parseWeeksFromSkzc(change.SKZC);
|
|
|
|
|
+ affected.forEach(course => {
|
|
|
|
|
+ const before = course.weeks.length;
|
|
|
|
|
+ course.weeks = course.weeks.filter(w => !weeksToRemove.includes(w));
|
|
|
|
|
+ if (course.weeks.length < before) changeApplied = true;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 落位到新时间/地点(新时间字段齐全即为调课,缺字段的即为停课)
|
|
|
|
|
+ const newWeeks = parseWeeksFromSkzc(change.XSKZC);
|
|
|
|
|
+ const newDay = parseInt(change.XSKXQ, 10);
|
|
|
|
|
+ const newStart = parseInt(change.XKSJC, 10);
|
|
|
|
|
+ const newEnd = parseInt(change.XJSJC, 10);
|
|
|
|
|
+ if (newWeeks.length > 0 && newDay && newStart && newEnd) {
|
|
|
|
|
+ const newPosition = change.XJASMC || change.JASMC || "待定";
|
|
|
|
|
+ // 同一 KBID 的同一课位(含教室)已存在则并入周次,避免产生重复条目
|
|
|
|
|
+ const existing = parsedCourses.find(c =>
|
|
|
|
|
+ c._kbId === kbID &&
|
|
|
|
|
+ c.day === newDay &&
|
|
|
|
|
+ c.startSection === newStart &&
|
|
|
|
|
+ c.endSection === newEnd &&
|
|
|
|
|
+ c.position === newPosition
|
|
|
|
|
+ );
|
|
|
|
|
+ if (existing) {
|
|
|
|
|
+ existing.weeks = [...new Set([...existing.weeks, ...newWeeks])].sort((a, b) => a - b);
|
|
|
|
|
+ } else {
|
|
|
parsedCourses.push({
|
|
parsedCourses.push({
|
|
|
name: change.KCM,
|
|
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),
|
|
|
|
|
|
|
+ teacher: cleanTeacher(change.XSKJS) || cleanTeacher(change.YSKJS) || "未知教师",
|
|
|
|
|
+ position: newPosition,
|
|
|
|
|
+ day: newDay,
|
|
|
|
|
+ startSection: newStart,
|
|
|
|
|
+ endSection: newEnd,
|
|
|
weeks: newWeeks,
|
|
weeks: newWeeks,
|
|
|
- _kbId: kbID,
|
|
|
|
|
- _day: parseInt(change.XSKXQ, 10),
|
|
|
|
|
- _startSection: parseInt(change.XKSJC, 10),
|
|
|
|
|
- _endSection: parseInt(change.XJSJC, 10)
|
|
|
|
|
|
|
+ _kbId: kbID
|
|
|
});
|
|
});
|
|
|
- changeApplied = true;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ changeApplied = true;
|
|
|
}
|
|
}
|
|
|
if (changeApplied) appliedCount++;
|
|
if (changeApplied) appliedCount++;
|
|
|
}
|
|
}
|
|
@@ -273,7 +285,7 @@ function applyCourseChanges(parsedCourses, rawChanges) {
|
|
|
/** 保存前清除内部匹配字段 */
|
|
/** 保存前清除内部匹配字段 */
|
|
|
function stripInternalFields(courses) {
|
|
function stripInternalFields(courses) {
|
|
|
return courses.map(c => {
|
|
return courses.map(c => {
|
|
|
- delete c._kbId; delete c._day; delete c._startSection; delete c._endSection;
|
|
|
|
|
|
|
+ delete c._kbId;
|
|
|
return c;
|
|
return c;
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -332,26 +344,15 @@ async function runImportFlow() {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 4.5 调课/停课(防御性:失败或无数据则跳过,不影响已验证的主流程)
|
|
|
|
|
|
|
+ // 4.5 调课/停课(查询异常不阻塞主流程)
|
|
|
let finalCourses = courses;
|
|
let finalCourses = courses;
|
|
|
let changeNote = "";
|
|
let changeNote = "";
|
|
|
try {
|
|
try {
|
|
|
const changes = await fetchCourseChanges(sem.xnxqdm);
|
|
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.shiguangBridgePromise.showAlert(
|
|
|
|
|
- "调课提示",
|
|
|
|
|
- `检测到 ${changes.length} 条调课记录但未能自动应用。已导入原始课表,请对照教务页"调课信息"手动核对,如有出入请联系开发者。`,
|
|
|
|
|
- "知道了"
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const result = applyCourseChanges(courses, changes);
|
|
|
|
|
+ finalCourses = result.courses;
|
|
|
|
|
+ if (result.appliedCount > 0) {
|
|
|
|
|
+ changeNote = `已应用${result.appliedCount}条调课`;
|
|
|
}
|
|
}
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.warn("调课处理跳过:", e);
|
|
console.warn("调课处理跳过:", e);
|